From 5be1f63f04266b501285523a1b5baf07cef6382a Mon Sep 17 00:00:00 2001 From: ziming Date: Mon, 11 Aug 2025 10:04:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=9A=E7=AC=94=E7=AB=8B=E5=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/bo/order_bo.go | 1 + internal/biz/bo/product_bo.go | 1 + internal/biz/order.go | 15 +- internal/biz/voucher.go | 3 + internal/biz/wechat_notify.go | 10 +- .../biz/wechatrepo/bank_multi_activity.go | 9 + internal/conf/conf.pb.go | 241 +++---- internal/conf/conf.proto | 1 + internal/data/model/order.gen.go | 1 + internal/data/model/order_bak.gen.go | 1 + internal/data/model/product.gen.go | 13 +- internal/data/provider_set.go | 1 + internal/data/repoimpl/order.go | 7 +- internal/data/repoimpl/order_bak.go | 2 +- .../wechatrepoimpl/bank_multi_activity.go | 44 ++ internal/data/wechatrepoimpl/provider_set.go | 1 + internal/data/wx.go | 71 ++ internal/data/wx_test.go | 23 + .../pkg/wechat/srv/marketing/marketing.go | 61 ++ internal/pkg/wechat/srv/marketing/model.go | 13 + internal/pkg/wechat/srv/srv.go | 7 + internal/pkg/wechat/utils/wxpay_utility.go | 607 ++++++++++++++++++ 22 files changed, 1003 insertions(+), 130 deletions(-) create mode 100644 internal/biz/wechatrepo/bank_multi_activity.go create mode 100644 internal/data/wechatrepoimpl/bank_multi_activity.go create mode 100644 internal/data/wx.go create mode 100644 internal/data/wx_test.go create mode 100644 internal/pkg/wechat/srv/marketing/marketing.go create mode 100644 internal/pkg/wechat/srv/marketing/model.go create mode 100644 internal/pkg/wechat/srv/srv.go create mode 100644 internal/pkg/wechat/utils/wxpay_utility.go diff --git a/internal/biz/bo/order_bo.go b/internal/biz/bo/order_bo.go index 1f6d9ef..f7c9cfe 100644 --- a/internal/biz/bo/order_bo.go +++ b/internal/biz/bo/order_bo.go @@ -13,6 +13,7 @@ type OrderBo struct { VoucherNo string ProductNo string BatchNo string + ActivityId string Account string Type vo.OrderType AccountType vo.OrderAccountType diff --git a/internal/biz/bo/product_bo.go b/internal/biz/bo/product_bo.go index 1cd43eb..025e1a4 100644 --- a/internal/biz/bo/product_bo.go +++ b/internal/biz/bo/product_bo.go @@ -12,6 +12,7 @@ type ProductBo struct { ProductNo string BatchName string BatchNo string + ActivityId string MchId string Channel vo.Channel AvailableType vo.AvailableType diff --git a/internal/biz/order.go b/internal/biz/order.go index ebac51c..e56760c 100644 --- a/internal/biz/order.go +++ b/internal/biz/order.go @@ -47,7 +47,7 @@ func (this *VoucherBiz) order(ctx context.Context, req *bo.OrderCreateReqBo, pro return nil, err } - voucherNo, err := this.WechatCpnRepo.Order(ctx, order) + voucherNo, err := this.send(ctx, order) if err != nil { if err3 := this.fail(ctx, order, err); err3 != nil { return nil, err3 @@ -62,9 +62,18 @@ func (this *VoucherBiz) order(ctx context.Context, req *bo.OrderCreateReqBo, pro return order, nil } +func (this *VoucherBiz) send(ctx context.Context, order *bo.OrderBo) (string, error) { + + if order.ActivityId == "" { + return this.WechatCpnRepo.Order(ctx, order) + } + + return this.BankMultiActivityRepo.Order(order) +} + func (this *VoucherBiz) orderRetry(ctx context.Context, order *bo.OrderBo) error { - voucherNo, err := this.WechatCpnRepo.Order(ctx, order) + voucherNo, err := this.send(ctx, order) if err != nil { if err3 := this.fail(ctx, order, err); err3 != nil { @@ -92,6 +101,8 @@ func (this *VoucherBiz) create(ctx context.Context, req *bo.OrderCreateReqBo, pr Type: req.Type, Status: vo.OrderStatusIng, // 同步发放,状态至为发放中 Attach: req.Attach, + + ActivityId: product.ActivityId, // 多笔立减活动 } return this.OrderRepo.Create(ctx, o) diff --git a/internal/biz/voucher.go b/internal/biz/voucher.go index c57ba0c..e21e639 100644 --- a/internal/biz/voucher.go +++ b/internal/biz/voucher.go @@ -22,6 +22,7 @@ type VoucherBiz struct { MqSendMixRepo mixrepos.MQSendMixRepo GenerateMixRepo mixrepos.GenerateMixRepo WechatCpnRepo wechatrepo.WechatCpnRepo + BankMultiActivityRepo wechatrepo.BankMultiActivityRepo DingMixRepo mixrepos.DingMixRepo CmbMixRepo mixrepos.CmbMixRepo SmsMixRepo mixrepos.SmsMixRepo @@ -42,6 +43,7 @@ func NewVoucherBiz( MqSendMixRepo mixrepos.MQSendMixRepo, GenerateMixRepo mixrepos.GenerateMixRepo, WechatCpnRepo wechatrepo.WechatCpnRepo, + BankMultiActivityRepo wechatrepo.BankMultiActivityRepo, DingMixRepo mixrepos.DingMixRepo, CmbMixRepo mixrepos.CmbMixRepo, SmsMixRepo mixrepos.SmsMixRepo, @@ -58,6 +60,7 @@ func NewVoucherBiz( MqSendMixRepo: MqSendMixRepo, GenerateMixRepo: GenerateMixRepo, WechatCpnRepo: WechatCpnRepo, + BankMultiActivityRepo: BankMultiActivityRepo, DingMixRepo: DingMixRepo, CmbMixRepo: CmbMixRepo, SmsMixRepo: SmsMixRepo, diff --git a/internal/biz/wechat_notify.go b/internal/biz/wechat_notify.go index 5aee3be..aebf079 100644 --- a/internal/biz/wechat_notify.go +++ b/internal/biz/wechat_notify.go @@ -127,10 +127,14 @@ func (this *VoucherBiz) cmbNotify(ctx context.Context, orderId uint64) error { return err } - if orderNotify, err := this.Cmb.Notify(ctx, order); err != nil { + if order.ActivityId == "" { + return nil // 多笔立减活动,不做通知(?不知道有没有核销通知,多笔核销情况未知) + } - if !errPb.IsNeedRetryNotify(err) { - return err + if orderNotify, err2 := this.Cmb.Notify(ctx, order); err != nil { + + if !errPb.IsNeedRetryNotify(err2) { + return err2 } // 第一次通知失败重试入队 diff --git a/internal/biz/wechatrepo/bank_multi_activity.go b/internal/biz/wechatrepo/bank_multi_activity.go new file mode 100644 index 0000000..640efbf --- /dev/null +++ b/internal/biz/wechatrepo/bank_multi_activity.go @@ -0,0 +1,9 @@ +package wechatrepo + +import ( + "voucher/internal/biz/bo" +) + +type BankMultiActivityRepo interface { + Order(order *bo.OrderBo) (couponId string, err error) +} diff --git a/internal/conf/conf.pb.go b/internal/conf/conf.pb.go index ea34485..da881e7 100644 --- a/internal/conf/conf.pb.go +++ b/internal/conf/conf.pb.go @@ -408,6 +408,7 @@ type Wechat struct { MchID string `protobuf:"bytes,1,opt,name=mchID,proto3" json:"mchID,omitempty"` MchCertificateSerialNumber string `protobuf:"bytes,2,opt,name=mchCertificateSerialNumber,proto3" json:"mchCertificateSerialNumber,omitempty"` WechatPayPublicKeyID string `protobuf:"bytes,3,opt,name=wechatPayPublicKeyID,proto3" json:"wechatPayPublicKeyID,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` } func (x *Wechat) Reset() { @@ -463,6 +464,13 @@ func (x *Wechat) GetWechatPayPublicKeyID() string { return "" } +func (x *Wechat) GetName() string { + if x != nil { + return x.Name + } + return "" +} + type Cmb struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1592,7 +1600,7 @@ var file_conf_conf_proto_rawDesc = []byte{ 0x6f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x65, 0x43, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6f, 0x6e, 0x73, 0x75, - 0x6d, 0x65, 0x72, 0x22, 0x92, 0x01, 0x0a, 0x06, 0x57, 0x65, 0x63, 0x68, 0x61, 0x74, 0x12, 0x14, + 0x6d, 0x65, 0x72, 0x22, 0xa6, 0x01, 0x0a, 0x06, 0x57, 0x65, 0x63, 0x68, 0x61, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x63, 0x68, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x63, 0x68, 0x49, 0x44, 0x12, 0x3e, 0x0a, 0x1a, 0x6d, 0x63, 0x68, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x4e, 0x75, 0x6d, 0x62, @@ -1601,121 +1609,122 @@ var file_conf_conf_proto_rawDesc = []byte{ 0x6d, 0x62, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x14, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x50, 0x61, 0x79, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x50, 0x61, 0x79, 0x50, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x49, 0x44, 0x22, 0xd7, 0x02, 0x0a, 0x03, 0x43, 0x6d, 0x62, - 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, - 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x61, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6d, 0x32, 0x50, 0x72, 0x6b, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6d, 0x32, 0x50, 0x72, 0x6b, 0x12, 0x16, 0x0a, 0x06, - 0x73, 0x6d, 0x32, 0x50, 0x75, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6d, - 0x32, 0x50, 0x75, 0x6b, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6d, 0x62, 0x53, 0x6d, 0x32, 0x50, 0x69, - 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6d, 0x62, 0x53, 0x6d, 0x32, 0x50, - 0x69, 0x6b, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6d, 0x62, 0x53, 0x6d, 0x32, 0x50, 0x75, 0x6b, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6d, 0x62, 0x53, 0x6d, 0x32, 0x50, 0x75, 0x6b, - 0x12, 0x1a, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x20, 0x0a, 0x0b, - 0x63, 0x6d, 0x62, 0x4b, 0x65, 0x79, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0b, 0x63, 0x6d, 0x62, 0x4b, 0x65, 0x79, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x14, - 0x0a, 0x05, 0x6f, 0x72, 0x67, 0x4e, 0x6f, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, - 0x72, 0x67, 0x4e, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x55, 0x72, - 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x55, - 0x72, 0x6c, 0x12, 0x28, 0x0a, 0x0f, 0x6e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x72, - 0x74, 0x44, 0x61, 0x79, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6e, 0x6f, 0x74, - 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x61, 0x79, 0x73, 0x12, 0x24, 0x0a, 0x0d, - 0x6e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x61, 0x79, 0x73, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x61, - 0x79, 0x73, 0x22, 0xc6, 0x02, 0x0a, 0x0e, 0x57, 0x65, 0x63, 0x68, 0x61, 0x74, 0x4e, 0x6f, 0x74, - 0x69, 0x66, 0x79, 0x4d, 0x51, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, - 0x65, 0x79, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x0f, 0x61, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, - 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1a, 0x0a, - 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x6e, 0x73, - 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, - 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, - 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, - 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, - 0x67, 0x12, 0x18, 0x0a, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x54, 0x61, 0x67, 0x55, 0x72, 0x6c, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x54, 0x61, 0x67, - 0x55, 0x72, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6f, 0x6e, - 0x73, 0x75, 0x6d, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, 0x73, 0x4f, - 0x70, 0x65, 0x6e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x72, 0x22, 0x9b, 0x01, 0x0a, 0x05, - 0x41, 0x6c, 0x61, 0x72, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, - 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x65, 0x62, 0x68, 0x6f, - 0x6f, 0x6b, 0x55, 0x52, 0x4c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x14, 0x0a, - 0x05, 0x61, 0x74, 0x41, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x61, 0x74, - 0x41, 0x6c, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x74, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x73, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x74, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, - 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x62, 0x69, - 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x61, 0x72, 0x6e, 0x69, - 0x6e, 0x67, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x84, 0x02, 0x0a, 0x04, 0x43, 0x72, - 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x12, 0x44, 0x0a, 0x0a, 0x63, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x61, 0x70, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, - 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, - 0x43, 0x72, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x61, 0x70, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x61, 0x70, - 0x1a, 0x3e, 0x0a, 0x0a, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x61, 0x70, 0x12, 0x16, - 0x0a, 0x06, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, - 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x1a, 0x5e, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x61, 0x70, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x43, 0x72, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x4d, 0x61, 0x70, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x22, 0xbe, 0x03, 0x0a, 0x05, 0x52, 0x64, 0x73, 0x4d, 0x51, 0x12, 0x3d, 0x0a, 0x0b, 0x77, 0x65, - 0x63, 0x68, 0x61, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1b, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x2e, 0x52, 0x64, 0x73, 0x4d, 0x51, 0x2e, 0x51, 0x75, 0x65, 0x75, 0x65, 0x52, 0x0b, 0x77, 0x65, - 0x63, 0x68, 0x61, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x4f, 0x0a, 0x14, 0x77, 0x65, 0x63, - 0x68, 0x61, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x51, 0x75, 0x65, 0x72, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, - 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x64, 0x73, 0x4d, 0x51, 0x2e, 0x51, - 0x75, 0x65, 0x75, 0x65, 0x52, 0x14, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x54, 0x69, 0x6d, 0x65, - 0x53, 0x6c, 0x69, 0x63, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x3d, 0x0a, 0x0b, 0x77, 0x65, - 0x63, 0x68, 0x61, 0x74, 0x52, 0x65, 0x74, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1b, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x2e, 0x52, 0x64, 0x73, 0x4d, 0x51, 0x2e, 0x51, 0x75, 0x65, 0x75, 0x65, 0x52, 0x0b, 0x77, 0x65, - 0x63, 0x68, 0x61, 0x74, 0x52, 0x65, 0x74, 0x72, 0x79, 0x12, 0x3d, 0x0a, 0x0b, 0x72, 0x65, 0x74, - 0x72, 0x79, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, - 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, - 0x52, 0x64, 0x73, 0x4d, 0x51, 0x2e, 0x51, 0x75, 0x65, 0x75, 0x65, 0x52, 0x0b, 0x72, 0x65, 0x74, - 0x72, 0x79, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x1a, 0xa6, 0x01, 0x0a, 0x05, 0x51, 0x75, 0x65, - 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x12, 0x1a, - 0x0a, 0x08, 0x72, 0x65, 0x74, 0x72, 0x79, 0x4e, 0x75, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x08, 0x72, 0x65, 0x74, 0x72, 0x79, 0x4e, 0x75, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x6e, 0x75, - 0x6d, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, - 0x6e, 0x75, 0x6d, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x12, 0x35, 0x0a, 0x08, 0x77, 0x61, - 0x69, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, - 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x77, 0x61, 0x69, 0x74, 0x54, 0x69, 0x6d, - 0x65, 0x22, 0xb9, 0x01, 0x0a, 0x09, 0x41, 0x6c, 0x69, 0x59, 0x75, 0x6e, 0x53, 0x6d, 0x73, 0x12, - 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x49, - 0x64, 0x12, 0x28, 0x0a, 0x0f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x53, 0x65, - 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, - 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, - 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x69, 0x67, 0x6e, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x69, 0x67, 0x6e, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x57, - 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0x3a, 0x0a, - 0x04, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x62, 0x75, 0x73, 0x69, 0x6e, 0x65, 0x73, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x62, 0x75, 0x73, 0x69, 0x6e, 0x65, 0x73, - 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x17, 0x5a, 0x15, 0x76, 0x6f, 0x75, - 0x63, 0x68, 0x65, 0x72, 0x2f, 0x63, 0x70, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x3b, 0x63, 0x6f, - 0x6e, 0x66, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xd7, 0x02, 0x0a, + 0x03, 0x43, 0x6d, 0x62, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6d, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6d, 0x32, 0x50, + 0x72, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6d, 0x32, 0x50, 0x72, 0x6b, + 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6d, 0x32, 0x50, 0x75, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x73, 0x6d, 0x32, 0x50, 0x75, 0x6b, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6d, 0x62, 0x53, + 0x6d, 0x32, 0x50, 0x69, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6d, 0x62, + 0x53, 0x6d, 0x32, 0x50, 0x69, 0x6b, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6d, 0x62, 0x53, 0x6d, 0x32, + 0x50, 0x75, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6d, 0x62, 0x53, 0x6d, + 0x32, 0x50, 0x75, 0x6b, 0x12, 0x1a, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x69, 0x61, 0x73, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6b, 0x65, 0x79, 0x41, 0x6c, 0x69, 0x61, 0x73, + 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6d, 0x62, 0x4b, 0x65, 0x79, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6d, 0x62, 0x4b, 0x65, 0x79, 0x41, 0x6c, 0x69, + 0x61, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x72, 0x67, 0x4e, 0x6f, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x4e, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x6f, 0x74, 0x69, + 0x66, 0x79, 0x55, 0x72, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x6f, 0x74, + 0x69, 0x66, 0x79, 0x55, 0x72, 0x6c, 0x12, 0x28, 0x0a, 0x0f, 0x6e, 0x6f, 0x74, 0x69, 0x63, 0x65, + 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x61, 0x79, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0f, 0x6e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x53, 0x74, 0x61, 0x72, 0x74, 0x44, 0x61, 0x79, 0x73, + 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x64, 0x44, 0x61, 0x79, + 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x45, + 0x6e, 0x64, 0x44, 0x61, 0x79, 0x73, 0x22, 0xc6, 0x02, 0x0a, 0x0e, 0x57, 0x65, 0x63, 0x68, 0x61, + 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x4d, 0x51, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, + 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x0f, 0x61, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x50, 0x6f, 0x69, 0x6e, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x50, 0x6f, 0x69, 0x6e, + 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1e, 0x0a, + 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, + 0x70, 0x69, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, + 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x54, 0x61, 0x67, 0x55, 0x72, + 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, + 0x72, 0x54, 0x61, 0x67, 0x55, 0x72, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x69, 0x73, 0x4f, 0x70, 0x65, + 0x6e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0e, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6d, 0x65, 0x72, 0x22, + 0x9b, 0x01, 0x0a, 0x05, 0x41, 0x6c, 0x61, 0x72, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x77, 0x65, 0x62, + 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, + 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x55, 0x52, 0x4c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x41, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x05, 0x61, 0x74, 0x41, 0x6c, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x74, 0x4d, 0x6f, 0x62, + 0x69, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x74, 0x4d, 0x6f, + 0x62, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0e, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, + 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x77, + 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x62, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x84, 0x02, + 0x0a, 0x04, 0x43, 0x72, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x12, 0x44, + 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x61, 0x70, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x2e, 0x43, 0x72, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x4d, 0x61, 0x70, 0x1a, 0x3e, 0x0a, 0x0a, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, + 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x1a, 0x5e, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, + 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, + 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x43, 0x72, 0x6f, 0x6e, 0x2e, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4d, 0x61, 0x70, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0xbe, 0x03, 0x0a, 0x05, 0x52, 0x64, 0x73, 0x4d, 0x51, 0x12, 0x3d, + 0x0a, 0x0b, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x64, 0x73, 0x4d, 0x51, 0x2e, 0x51, 0x75, 0x65, 0x75, 0x65, + 0x52, 0x0b, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x4f, 0x0a, + 0x14, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x6c, 0x69, 0x63, 0x65, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x76, 0x6f, + 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x64, 0x73, + 0x4d, 0x51, 0x2e, 0x51, 0x75, 0x65, 0x75, 0x65, 0x52, 0x14, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, + 0x54, 0x69, 0x6d, 0x65, 0x53, 0x6c, 0x69, 0x63, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x3d, + 0x0a, 0x0b, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x52, 0x65, 0x74, 0x72, 0x79, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x64, 0x73, 0x4d, 0x51, 0x2e, 0x51, 0x75, 0x65, 0x75, 0x65, + 0x52, 0x0b, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x52, 0x65, 0x74, 0x72, 0x79, 0x12, 0x3d, 0x0a, + 0x0b, 0x72, 0x65, 0x74, 0x72, 0x79, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x2e, 0x52, 0x64, 0x73, 0x4d, 0x51, 0x2e, 0x51, 0x75, 0x65, 0x75, 0x65, 0x52, + 0x0b, 0x72, 0x65, 0x74, 0x72, 0x79, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x1a, 0xa6, 0x01, 0x0a, + 0x05, 0x51, 0x75, 0x65, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, + 0x4f, 0x70, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, 0x73, 0x4f, 0x70, + 0x65, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x74, 0x72, 0x79, 0x4e, 0x75, 0x6d, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x72, 0x65, 0x74, 0x72, 0x79, 0x4e, 0x75, 0x6d, 0x12, 0x1e, + 0x0a, 0x0a, 0x6e, 0x75, 0x6d, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x0a, 0x6e, 0x75, 0x6d, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x12, 0x35, + 0x0a, 0x08, 0x77, 0x61, 0x69, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x77, 0x61, 0x69, + 0x74, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xb9, 0x01, 0x0a, 0x09, 0x41, 0x6c, 0x69, 0x59, 0x75, 0x6e, + 0x53, 0x6d, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, + 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x0f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, + 0x65, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, + 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, + 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, + 0x69, 0x67, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, + 0x69, 0x67, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0f, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, + 0x67, 0x22, 0x3a, 0x0a, 0x04, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x62, 0x75, 0x73, + 0x69, 0x6e, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x62, 0x75, 0x73, + 0x69, 0x6e, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x42, 0x17, 0x5a, + 0x15, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2f, 0x63, 0x70, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, + 0x66, 0x3b, 0x63, 0x6f, 0x6e, 0x66, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/conf/conf.proto b/internal/conf/conf.proto index 985c0da..58ee42f 100644 --- a/internal/conf/conf.proto +++ b/internal/conf/conf.proto @@ -74,6 +74,7 @@ message Wechat { string mchID = 1; string mchCertificateSerialNumber = 2; string wechatPayPublicKeyID = 3; + string name = 4; } message Cmb { diff --git a/internal/data/model/order.gen.go b/internal/data/model/order.gen.go index f078044..cf35488 100644 --- a/internal/data/model/order.gen.go +++ b/internal/data/model/order.gen.go @@ -18,6 +18,7 @@ type Order struct { OutBizNo string `gorm:"column:out_biz_no;not null;comment:外部交易号" json:"out_biz_no"` // 外部交易号 ProductNo string `gorm:"column:product_no;not null;comment:商品编号" json:"product_no"` // 商品编号 BatchNo string `gorm:"column:batch_no;not null;comment:立减金批次号" json:"batch_no"` // 立减金批次号 + ActivityId string `gorm:"column:activity_id;not null;comment:activity_id" json:"activity_id"` // activity_id Account string `gorm:"column:account;not null;comment:充值账号" json:"account"` // 充值账号 AccountType uint8 `gorm:"column:account_type;not null;comment:1:oepnid/userid 2:手机号" json:"account_type"` // 1:oepnid/userid 2:手机号 Type uint8 `gorm:"column:type;not null;comment:1:招行" json:"type"` diff --git a/internal/data/model/order_bak.gen.go b/internal/data/model/order_bak.gen.go index 18a005e..42765ae 100644 --- a/internal/data/model/order_bak.gen.go +++ b/internal/data/model/order_bak.gen.go @@ -18,6 +18,7 @@ type OrderBak struct { OutBizNo string `gorm:"column:out_biz_no;not null;comment:外部交易号" json:"out_biz_no"` // 外部交易号 ProductNo string `gorm:"column:product_no;not null;comment:商品编号" json:"product_no"` // 商品编号 BatchNo string `gorm:"column:batch_no;not null;comment:立减金批次号" json:"batch_no"` // 立减金批次号 + ActivityId string `gorm:"column:activity_id;not null;comment:activity_id" json:"activity_id"` // activity_id Account string `gorm:"column:account;not null;comment:充值账号" json:"account"` // 充值账号 AccountType uint8 `gorm:"column:account_type;not null;comment:1:oepnid/userid 2:手机号" json:"account_type"` // 1:oepnid/userid 2:手机号 Type uint8 `gorm:"column:type;not null;comment:1:招行" json:"type"` diff --git a/internal/data/model/product.gen.go b/internal/data/model/product.gen.go index ac3dd3f..1c27899 100644 --- a/internal/data/model/product.gen.go +++ b/internal/data/model/product.gen.go @@ -13,12 +13,13 @@ const TableNameProduct = "product" // Product mapped from table type Product struct { ID int32 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"` - Name string `gorm:"column:name;not null;comment:商品名称" json:"name"` // 商品名称 - ProductNo string `gorm:"column:product_no;not null;comment:商品编号" json:"product_no"` // 商品编号 - BatchName string `gorm:"column:batch_name;not null;comment:批次名称" json:"batch_name"` // 批次名称 - BatchNo string `gorm:"column:batch_no;not null;comment:立减金批次号" json:"batch_no"` // 立减金批次号 - MchId string `gorm:"column:mch_id;not null;comment:商户号,创建批次的商户号" json:"mch_id"` // 商户号,创建批次的商户号 - Channel uint8 `gorm:"column:channel;not null;comment:1:微信 2:支付宝" json:"channel"` // 1:微信 2:支付宝 + Name string `gorm:"column:name;not null;comment:商品名称" json:"name"` // 商品名称 + ProductNo string `gorm:"column:product_no;not null;comment:商品编号" json:"product_no"` // 商品编号 + BatchName string `gorm:"column:batch_name;not null;comment:批次名称" json:"batch_name"` // 批次名称 + BatchNo string `gorm:"column:batch_no;not null;comment:立减金批次号" json:"batch_no"` // 立减金批次号 + ActivityId string `gorm:"column:activity_id;not null;comment:activity_id" json:"activity_id"` // activity_id + MchId string `gorm:"column:mch_id;not null;comment:商户号,创建批次的商户号" json:"mch_id"` // 商户号,创建批次的商户号 + Channel uint8 `gorm:"column:channel;not null;comment:1:微信 2:支付宝" json:"channel"` // 1:微信 2:支付宝 AvailableType uint8 `gorm:"column:available_type;not null;comment:1:固定有效期 2:动态有效期" json:"available_type"` AvailableDays uint32 `gorm:"column:available_days;not null;comment:领取后多少天内" json:"available_days"` Amount int64 `gorm:"column:amount;not null;default:0" json:"amount"` diff --git a/internal/data/provider_set.go b/internal/data/provider_set.go index 1808fd1..ae54a9d 100644 --- a/internal/data/provider_set.go +++ b/internal/data/provider_set.go @@ -10,4 +10,5 @@ var ProviderDataSet = wire.NewSet( NewDb, NewRdb, NewRocketMQ, + NewWx, ) diff --git a/internal/data/repoimpl/order.go b/internal/data/repoimpl/order.go index bf73fe5..7033a07 100644 --- a/internal/data/repoimpl/order.go +++ b/internal/data/repoimpl/order.go @@ -33,7 +33,7 @@ func (p *OrderRepoImpl) DB(ctx context.Context) *gorm.DB { func (p *OrderRepoImpl) SpecifyFindInBatches(ctx context.Context, req *bo.FindInBatchesBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error { - tx := p.DB(ctx) + tx := p.DB(ctx).Where("activity_id = ''") if req.ProductNo != "" { tx = tx.Where("product_no = ?", req.ProductNo) @@ -71,7 +71,7 @@ func (p *OrderRepoImpl) SpecifyFindInBatches(ctx context.Context, req *bo.FindIn func (p *OrderRepoImpl) FinSucByStockIdInBatches(ctx context.Context, req *do.WechatQuery, fun func(ctx context.Context, rows []*bo.OrderBo) error) error { - tx := p.DB(ctx).Where("status = ?", vo.OrderStatusSuccess.GetValue()) + tx := p.DB(ctx).Where("status = ?", vo.OrderStatusSuccess.GetValue()).Where("activity_id = ''") if req.ProductNo != "" { tx = tx.Where("product_no = ?", req.ProductNo) @@ -144,6 +144,7 @@ func (p *OrderRepoImpl) FindInBatches(ctx context.Context, req *bo.FindInBatches var results = make([]*model.Order, 0) result := p.DB(ctx). + Where("activity_id = ''"). Where("status IN (?)", []uint8{vo.OrderStatusSuccess.GetValue(), vo.OrderStatusUse.GetValue()}). Where("receive_success_time BETWEEN ? AND ?", req.StartTime, req.EndTime). FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error { @@ -180,6 +181,8 @@ func (p *OrderRepoImpl) Create(ctx context.Context, req *bo.OrderBo) (*bo.OrderB Attach: req.Attach, CreateTime: &now, UpdateTime: &now, + + ActivityId: req.ActivityId, } tx := p.DB(ctx).Create(info) diff --git a/internal/data/repoimpl/order_bak.go b/internal/data/repoimpl/order_bak.go index 14f0bdf..6509075 100644 --- a/internal/data/repoimpl/order_bak.go +++ b/internal/data/repoimpl/order_bak.go @@ -26,7 +26,7 @@ func (p *OrderBakRepoImpl) DB(ctx context.Context) *gorm.DB { func (p *OrderBakRepoImpl) SpecifyFindInBatches(ctx context.Context, req *bo.FindInBatchesBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error { - tx := p.DB(ctx) + tx := p.DB(ctx).Where("activity_id = ''") if req.ProductNo != "" { tx = tx.Where("product_no = ?", req.ProductNo) diff --git a/internal/data/wechatrepoimpl/bank_multi_activity.go b/internal/data/wechatrepoimpl/bank_multi_activity.go new file mode 100644 index 0000000..9410918 --- /dev/null +++ b/internal/data/wechatrepoimpl/bank_multi_activity.go @@ -0,0 +1,44 @@ +package wechatrepoimpl + +import ( + "github.com/wechatpay-apiv3/wechatpay-go/core" + "voucher/internal/biz/bo" + "voucher/internal/biz/wechatrepo" + "voucher/internal/conf" + "voucher/internal/data" + "voucher/internal/pkg/wechat/srv/marketing" +) + +type BankMultiActivityImpl struct { + bc *conf.Bootstrap + wx *data.Wx +} + +func NewBankMultiActivityImpl(bc *conf.Bootstrap, wx *data.Wx) wechatrepo.BankMultiActivityRepo { + return &BankMultiActivityImpl{bc: bc, wx: wx} +} + +func (w *BankMultiActivityImpl) Order(order *bo.OrderBo) (couponId string, err error) { + + req := &marketing.SendReq{ + ActivityId: core.String(order.ActivityId), + StockId: core.String(order.BatchNo), + OutRequestNo: core.String(order.OrderNo), + Appid: core.String(order.AppID), + StockCreatorMchId: core.String(order.MerchantNo), + } + + t, err := w.wx.Get(w.bc.Wechat.MchID) + if err != nil { + return "", err + } + + resp, err := t.Send(order.Account, req) + + if err != nil { + + return "", err + } + + return *resp.CouponId, nil +} diff --git a/internal/data/wechatrepoimpl/provider_set.go b/internal/data/wechatrepoimpl/provider_set.go index 5b89fe6..b747a09 100644 --- a/internal/data/wechatrepoimpl/provider_set.go +++ b/internal/data/wechatrepoimpl/provider_set.go @@ -7,4 +7,5 @@ import ( // ProviderWechatReposImplSet is providers. var ProviderWechatReposImplSet = wire.NewSet( NewCpnRepoImpl, + NewBankMultiActivityImpl, ) diff --git a/internal/data/wx.go b/internal/data/wx.go new file mode 100644 index 0000000..0c8ce17 --- /dev/null +++ b/internal/data/wx.go @@ -0,0 +1,71 @@ +package data + +import ( + "fmt" + "os" + "voucher/internal/conf" + "voucher/internal/pkg/helper" + "voucher/internal/pkg/wechat/srv/marketing" + utils2 "voucher/internal/pkg/wechat/utils" +) + +type Wx struct { + Clients map[string]*marketing.Marketing +} + +func NewWx(c *conf.Bootstrap) (*Wx, error) { + + clients := make(map[string]*marketing.Marketing, 0) + + client, err := buildWx(c.Wechat) + if err != nil { + return nil, err + } + + clients[c.Wechat.MchID] = client + + return &Wx{Clients: clients}, nil +} + +func (this *Wx) Get(mchId string) (*marketing.Marketing, error) { + + if mchId == "" { + return nil, fmt.Errorf("微信商户ID不能为空") + } + + if len(this.Clients) == 0 { + return nil, fmt.Errorf("微信调用client不存在") + } + + if client, ok := this.Clients[mchId]; ok { + return client, nil + } + + return nil, fmt.Errorf("微信调用client不存在[%s]", mchId) +} + +func buildWx(wx *conf.Wechat) (*marketing.Marketing, error) { + + dir, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("商户ID[%s]商户名称[%s]获取目的地址有误[%v]", wx.MchID, wx.Name, err) + } + + filePath := fmt.Sprintf("%s/cert/wechat/%s", dir, wx.MchID) + if !helper.FileExists(filePath) { + panic(fmt.Sprintf("商户ID[%s]商户名称[%s]微信密钥证书信息不存在,请联系技术人员处理", wx.MchID, wx.Name)) + } + + cc, err := utils2.CreateMchConfig( + wx.MchID, // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 + wx.MchCertificateSerialNumber, // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 + fmt.Sprintf("%s/%s", filePath, "apiclient_key.pem"), // 商户API证书私钥文件路径,本地文件路径 + wx.WechatPayPublicKeyID, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 + fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径 + ) + if err != nil { + return nil, err + } + + return &marketing.Marketing{MchConfig: cc}, nil +} diff --git a/internal/data/wx_test.go b/internal/data/wx_test.go new file mode 100644 index 0000000..3cc5f11 --- /dev/null +++ b/internal/data/wx_test.go @@ -0,0 +1,23 @@ +package data + +import ( + "testing" + "voucher/internal/conf" +) + +func TestNewWechat(t *testing.T) { + c := &conf.Bootstrap{ + Wechat: &conf.Wechat{ + MchID: "123456", + MchCertificateSerialNumber: "123456", + WechatPayPublicKeyID: "123456", + Name: "lsxd-test", + }, + } + got, err := NewWx(c) + if err != nil { + t.Errorf("NewWx() error = %v", err) + return + } + t.Logf("NewWx() = %v", got) +} diff --git a/internal/pkg/wechat/srv/marketing/marketing.go b/internal/pkg/wechat/srv/marketing/marketing.go new file mode 100644 index 0000000..65203e0 --- /dev/null +++ b/internal/pkg/wechat/srv/marketing/marketing.go @@ -0,0 +1,61 @@ +package marketing + +import ( + "encoding/json" + "net/url" + "strings" + "voucher/internal/pkg/wechat/srv" + "voucher/internal/pkg/wechat/utils" +) + +const ( + sendPath = "/v3/marketing/bank-favor/users/{openid}/bank-multi-activity" + queryPath = "/v3/marketing/bank-favor/users/{openid}/coupons/{coupon_id}" +) + +// Marketing 产品介绍 https://docs.qq.com/doc/DSUJPTEhrc0Npb2Zj?_t=1743576271598&nlc=1 +type Marketing srv.Srv + +// Send @link https://pay.weixin.qq.com/doc/v3/merchant/4014569793 +func (srv *Marketing) Send(openId string, req *SendReq) (response *SendResp, err error) { + + reqBody, err := json.Marshal(&req) + if err != nil { + return nil, err + } + + path := strings.Replace(sendPath, "{openid}", url.PathEscape(openId), -1) + + respBody, err := srv.Request2(utils.Host, utils.MethodPOST, path, reqBody) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(respBody, &response); err != nil { + return nil, err + } + + return response, nil +} + +// Query @link https://pay.weixin.qq.com/doc/v3/merchant/4014569864 +func (srv *Marketing) Query(appid, openId, couponId string) (response *SendResp, err error) { + + path := strings.Replace(queryPath, "{openid}", url.PathEscape(openId), -1) + path = strings.Replace(path, "{coupon_id}", url.PathEscape(couponId), -1) + + var uv = url.Values{} + uv.Set("appid", appid) + path += "?" + uv.Encode() + + respBody, err := srv.Request2(utils.Host, utils.MethodGET, path, nil) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(respBody, &response); err != nil { + return nil, err + } + + return response, nil +} diff --git a/internal/pkg/wechat/srv/marketing/model.go b/internal/pkg/wechat/srv/marketing/model.go new file mode 100644 index 0000000..71eab28 --- /dev/null +++ b/internal/pkg/wechat/srv/marketing/model.go @@ -0,0 +1,13 @@ +package marketing + +type SendReq struct { + ActivityId *string `json:"activity_id"` + StockId *string `json:"stock_id"` + OutRequestNo *string `json:"out_request_no"` + Appid *string `json:"appid"` + StockCreatorMchId *string `json:"stock_creator_mchid"` +} + +type SendResp struct { + CouponId *string `json:"coupon_id"` +} diff --git a/internal/pkg/wechat/srv/srv.go b/internal/pkg/wechat/srv/srv.go new file mode 100644 index 0000000..a088fc9 --- /dev/null +++ b/internal/pkg/wechat/srv/srv.go @@ -0,0 +1,7 @@ +package srv + +import "voucher/internal/pkg/wechat/utils" + +type Srv struct { + *utils.MchConfig +} diff --git a/internal/pkg/wechat/utils/wxpay_utility.go b/internal/pkg/wechat/utils/wxpay_utility.go new file mode 100644 index 0000000..8369952 --- /dev/null +++ b/internal/pkg/wechat/utils/wxpay_utility.go @@ -0,0 +1,607 @@ +package utils + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "sort" + "strconv" + "strings" + "time" +) + +const ( + Host = "https://api.mch.weixin.qq.com" + MethodGET = "GET" + MethodPOST = "POST" +) + +// MchConfig 商户信息配置,用于调用商户API +// https://pay.weixin.qq.com/doc/v3/merchant/4012716434 +// +// 引用微信支付工具库 参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334 +type MchConfig struct { + mchId string + certificateSerialNo string + privateKeyFilePath string + wechatPayPublicKeyId string + wechatPayPublicKeyFilePath string + privateKey *rsa.PrivateKey + wechatPayPublicKey *rsa.PublicKey +} + +// MchId 商户号 +func (c *MchConfig) MchId() string { + return c.mchId +} + +// CertificateSerialNo 商户API证书序列号 +func (c *MchConfig) CertificateSerialNo() string { + return c.certificateSerialNo +} + +// PrivateKey 商户API证书对应的私钥 +func (c *MchConfig) PrivateKey() *rsa.PrivateKey { + return c.privateKey +} + +// WechatPayPublicKeyId 微信支付公钥ID +func (c *MchConfig) WechatPayPublicKeyId() string { + return c.wechatPayPublicKeyId +} + +// WechatPayPublicKey 微信支付公钥 +func (c *MchConfig) WechatPayPublicKey() *rsa.PublicKey { + return c.wechatPayPublicKey +} + +// CreateMchConfig MchConfig 构造函数 +func CreateMchConfig( + mchId string, + certificateSerialNo string, + privateKeyFilePath string, + wechatPayPublicKeyId string, + wechatPayPublicKeyFilePath string, +) (*MchConfig, error) { + mchConfig := &MchConfig{ + mchId: mchId, + certificateSerialNo: certificateSerialNo, + privateKeyFilePath: privateKeyFilePath, + wechatPayPublicKeyId: wechatPayPublicKeyId, + wechatPayPublicKeyFilePath: wechatPayPublicKeyFilePath, + } + privateKey, err := LoadPrivateKeyWithPath(mchConfig.privateKeyFilePath) + if err != nil { + return nil, err + } + mchConfig.privateKey = privateKey + wechatPayPublicKey, err := LoadPublicKeyWithPath(mchConfig.wechatPayPublicKeyFilePath) + if err != nil { + return nil, err + } + mchConfig.wechatPayPublicKey = wechatPayPublicKey + return mchConfig, nil +} + +// LoadPrivateKey 通过私钥的文本内容加载私钥 +func LoadPrivateKey(privateKeyStr string) (privateKey *rsa.PrivateKey, err error) { + block, _ := pem.Decode([]byte(privateKeyStr)) + if block == nil { + return nil, fmt.Errorf("decode private key err") + } + if block.Type != "PRIVATE KEY" { + return nil, fmt.Errorf("the kind of PEM should be PRVATE KEY") + } + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse private key err:%s", err.Error()) + } + privateKey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("not a RSA private key") + } + return privateKey, nil +} + +// LoadPublicKey 通过公钥的文本内容加载公钥 +func LoadPublicKey(publicKeyStr string) (publicKey *rsa.PublicKey, err error) { + block, _ := pem.Decode([]byte(publicKeyStr)) + if block == nil { + return nil, errors.New("decode public key error") + } + if block.Type != "PUBLIC KEY" { + return nil, fmt.Errorf("the kind of PEM should be PUBLIC KEY") + } + key, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse public key err:%s", err.Error()) + } + publicKey, ok := key.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("%s is not rsa public key", publicKeyStr) + } + return publicKey, nil +} + +// LoadPrivateKeyWithPath 通过私钥的文件路径内容加载私钥 +func LoadPrivateKeyWithPath(path string) (privateKey *rsa.PrivateKey, err error) { + privateKeyBytes, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read private pem file err:%s", err.Error()) + } + return LoadPrivateKey(string(privateKeyBytes)) +} + +// LoadPublicKeyWithPath 通过公钥的文件路径加载公钥 +func LoadPublicKeyWithPath(path string) (publicKey *rsa.PublicKey, err error) { + publicKeyBytes, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read certificate pem file err:%s", err.Error()) + } + return LoadPublicKey(string(publicKeyBytes)) +} + +// EncryptOAEPWithPublicKey 使用 OAEP padding方式用公钥进行加密 +func EncryptOAEPWithPublicKey(message string, publicKey *rsa.PublicKey) (ciphertext string, err error) { + if publicKey == nil { + return "", fmt.Errorf("you should input *rsa.PublicKey") + } + ciphertextByte, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, publicKey, []byte(message), nil) + if err != nil { + return "", fmt.Errorf("encrypt message with public key err:%s", err.Error()) + } + ciphertext = base64.StdEncoding.EncodeToString(ciphertextByte) + return ciphertext, nil +} + +// SignSHA256WithRSA 通过私钥对字符串以 SHA256WithRSA 算法生成签名信息 +func SignSHA256WithRSA(source string, privateKey *rsa.PrivateKey) (signature string, err error) { + if privateKey == nil { + return "", fmt.Errorf("private key should not be nil") + } + h := crypto.Hash.New(crypto.SHA256) + _, err = h.Write([]byte(source)) + if err != nil { + return "", nil + } + hashed := h.Sum(nil) + signatureByte, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(signatureByte), nil +} + +// VerifySHA256WithRSA 通过公钥对字符串和签名结果以 SHA256WithRSA 验证签名有效性 +func VerifySHA256WithRSA(source string, signature string, publicKey *rsa.PublicKey) error { + if publicKey == nil { + return fmt.Errorf("public key should not be nil") + } + + sigBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return fmt.Errorf("verify failed: signature is not base64 encoded") + } + hashed := sha256.Sum256([]byte(source)) + err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], sigBytes) + if err != nil { + return fmt.Errorf("verify signature with public key error:%s", err.Error()) + } + return nil +} + +// GenerateNonce 生成一个长度为 NonceLength 的随机字符串(只包含大小写字母与数字) +func GenerateNonce() (string, error) { + const ( + // NonceSymbols 随机字符串可用字符集 + NonceSymbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + // NonceLength 随机字符串的长度 + NonceLength = 32 + ) + + bs := make([]byte, NonceLength) + _, err := rand.Read(bs) + if err != nil { + return "", err + } + symbolsByteLength := byte(len(NonceSymbols)) + for i, b := range bs { + bs[i] = NonceSymbols[b%symbolsByteLength] + } + return string(bs), nil +} + +// BuildAuthorization 构建请求头中的 Authorization 信息 +func BuildAuthorization( + mchid string, + certificateSerialNo string, + privateKey *rsa.PrivateKey, + method string, + canonicalURL string, + body []byte, +) (string, error) { + const ( + SignatureMessageFormat = "%s\n%s\n%d\n%s\n%s\n" // 数字签名原文格式 + // HeaderAuthorizationFormat 请求头中的 Authorization 拼接格式 + HeaderAuthorizationFormat = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"" + ) + + nonce, err := GenerateNonce() + if err != nil { + return "", err + } + timestamp := time.Now().Unix() + message := fmt.Sprintf(SignatureMessageFormat, method, canonicalURL, timestamp, nonce, body) + signature, err := SignSHA256WithRSA(message, privateKey) + if err != nil { + return "", err + } + authorization := fmt.Sprintf( + HeaderAuthorizationFormat, + mchid, nonce, timestamp, certificateSerialNo, signature, + ) + return authorization, nil +} + +// ExtractResponseBody 提取应答报文的 Body +func ExtractResponseBody(response *http.Response) ([]byte, error) { + if response.Body == nil { + return nil, nil + } + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf("read response HttpBody err:[%s]", err.Error()) + } + response.Body = io.NopCloser(bytes.NewBuffer(body)) + return body, nil +} + +const ( + WechatPayTimestamp = "Wechatpay-Timestamp" // 微信支付回包时间戳 + WechatPayNonce = "Wechatpay-Nonce" // 微信支付回包随机字符串 + WechatPaySignature = "Wechatpay-Signature" // 微信支付回包签名信息 + WechatPaySerial = "Wechatpay-Serial" // 微信支付回包平台序列号 + RequestID = "Request-Id" // 微信支付回包请求ID +) + +// ValidateResponse 验证微信支付回包的签名信息 +func ValidateResponse( + wechatpayPublicKeyId string, + wechatpayPublicKey *rsa.PublicKey, + headers *http.Header, + body []byte, +) error { + requestID := headers.Get(RequestID) + timestampStr := headers.Get(WechatPayTimestamp) + serialNo := headers.Get(WechatPaySerial) + signature := headers.Get(WechatPaySignature) + nonce := headers.Get(WechatPayNonce) + + // 拒绝过期请求 + timestamp, err := strconv.ParseInt(timestampStr, 10, 64) + if err != nil { + return fmt.Errorf("invalid timestamp: %v", err) + } + if time.Now().Sub(time.Unix(timestamp, 0)) > 5*time.Minute { + return errors.New("invalid timestamp") + } + + if serialNo != wechatpayPublicKeyId { + return fmt.Errorf( + "serial-no mismatch: got %s, expected %s, request-id: %s", + serialNo, + wechatpayPublicKeyId, + requestID, + ) + } + + message := fmt.Sprintf("%s\n%s\n%s\n", timestampStr, nonce, body) + if err := VerifySHA256WithRSA(message, signature, wechatpayPublicKey); err != nil { + return fmt.Errorf("invalid signature: %v, request-id: %s", err, requestID) + } + + return nil +} + +// ApiException 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 +type ApiException struct { + HttpStatusCode int // 应答报文的 HTTP 状态码 + HttpHeader http.Header // 应答报文的 Header 信息 + HttpBody []byte // 应答报文的 Body 原文 + ErrCode string // 微信支付回包的错误码 + ErrMessage string // 微信支付回包的错误信息 +} + +func (c *ApiException) Error() string { + buf := bytes.NewBuffer(nil) + buf.WriteString(fmt.Sprintf("srv error:[StatusCode: %d, Body: %s", c.HttpStatusCode, string(c.HttpBody))) + if len(c.HttpHeader) > 0 { + buf.WriteString(" Header: ") + for key, value := range c.HttpHeader { + buf.WriteString(fmt.Sprintf("\n - %v=%v", key, value)) + } + buf.WriteString("\n") + } + buf.WriteString("]") + return buf.String() +} + +func (c *ApiException) StatusCode() int { + return c.HttpStatusCode +} + +func (c *ApiException) Header() http.Header { + return c.HttpHeader +} + +func (c *ApiException) Body() []byte { + return c.HttpBody +} + +func (c *ApiException) ErrorCode() string { + return c.ErrCode +} + +func (c *ApiException) ErrorMessage() string { + return c.ErrMessage +} + +func NewApiException(statusCode int, header http.Header, body []byte) error { + ret := &ApiException{ + HttpStatusCode: statusCode, + HttpHeader: header, + HttpBody: body, + } + + bodyObject := map[string]interface{}{} + if err := json.Unmarshal(body, &bodyObject); err == nil { + if val, ok := bodyObject["code"]; ok { + ret.ErrCode = val.(string) + } + if val, ok := bodyObject["message"]; ok { + ret.ErrMessage = val.(string) + } + } + + return ret +} + +// Time 复制 time.Time 对象,并返回复制体的指针 +func Time(t time.Time) *time.Time { + return &t +} + +// String 复制 string 对象,并返回复制体的指针 +func String(s string) *string { + return &s +} + +// Bool 复制 bool 对象,并返回复制体的指针 +func Bool(b bool) *bool { + return &b +} + +// Float64 复制 float64 对象,并返回复制体的指针 +func Float64(f float64) *float64 { + return &f +} + +// Float32 复制 float32 对象,并返回复制体的指针 +func Float32(f float32) *float32 { + return &f +} + +// Int64 复制 int64 对象,并返回复制体的指针 +func Int64(i int64) *int64 { + return &i +} + +// Int32 复制 int64 对象,并返回复制体的指针 +func Int32(i int32) *int32 { + return &i +} + +func (srv *MchConfig) Request(host, method, path string, reqBody []byte) (response []byte, err error) { + + reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path)) + if err != nil { + return nil, err + } + + fmt.Print(reqUrl.Path) + + httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody)) + + if err != nil { + return nil, err + } + + //httpRequest.Header.Set("mchid", srv.mchId) + httpRequest.Header.Set("Accept", "application/json") + httpRequest.Header.Set("Wechatpay-Serial", srv.WechatPayPublicKeyId()) + httpRequest.Header.Set("Content-Type", "application/json") + + authorization, err := BuildAuthorization( + srv.MchId(), + srv.CertificateSerialNo(), + srv.PrivateKey(), + method, + reqUrl.Path, + reqBody, + ) + if err != nil { + return nil, err + } + + httpRequest.Header.Set("Authorization", authorization) + + client := &http.Client{} + httpResponse, err := client.Do(httpRequest) + if err != nil { + return nil, err + } + + respBody, err := ExtractResponseBody(httpResponse) + if err != nil { + return nil, err + } + + if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 { + // 2XX 成功,验证应答签名 + err = ValidateResponse( + srv.WechatPayPublicKeyId(), + srv.WechatPayPublicKey(), + &httpResponse.Header, + respBody, + ) + if err != nil { + return nil, err + } + + return respBody, nil + } + + return nil, &ApiException{ + HttpStatusCode: httpResponse.StatusCode, + HttpHeader: httpResponse.Header, + HttpBody: respBody, + } +} + +func (srv *MchConfig) Request2(host, method, path string, reqBody []byte) (response []byte, err error) { + + reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path)) + if err != nil { + return nil, err + } + + httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody)) + + if err != nil { + return nil, err + } + + httpRequest.Header.Set("Accept", "application/json") + httpRequest.Header.Set("Wechatpay-Serial", srv.WechatPayPublicKeyId()) + httpRequest.Header.Set("Content-Type", "application/json") + httpRequest.Header.Set("mchid", "application/json") + + authorization, err := BuildAuthorization( + srv.MchId(), + srv.CertificateSerialNo(), + srv.PrivateKey(), + method, + path, + reqBody, + ) + if err != nil { + return nil, err + } + + httpRequest.Header.Set("Authorization", authorization) + + hs, _ := json.Marshal(httpRequest.Header) + fmt.Printf("\npath=%s\nreqBody=%s\nheaders=%s\n", path, string(reqBody), string(hs)) + + client := &http.Client{} + httpResponse, err := client.Do(httpRequest) + if err != nil { + return nil, err + } + + respBody, err := ExtractResponseBody(httpResponse) + if err != nil { + return nil, err + } + + if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 { + // 2XX 成功,验证应答签名 + err = ValidateResponse( + srv.WechatPayPublicKeyId(), + srv.WechatPayPublicKey(), + &httpResponse.Header, + respBody, + ) + if err != nil { + return nil, err + } + + return respBody, nil + } + + return nil, &ApiException{ + HttpStatusCode: httpResponse.StatusCode, + HttpHeader: httpResponse.Header, + HttpBody: respBody, + } +} + +func (srv *MchConfig) Verify(request *http.Request) (string, error) { + + respBody, err := io.ReadAll(request.Body) + if err != nil { + return "", fmt.Errorf("read request HttpBody err:[%s]", err.Error()) + } + + err = ValidateResponse( + srv.WechatPayPublicKeyId(), + srv.WechatPayPublicKey(), + &request.Header, + respBody, + ) + if err != nil { + return "", err + } + + return EncryptOAEPWithPublicKey(string(respBody), srv.wechatPayPublicKey) +} + +// BuildSortedQueryString 函数接受一个 map,返回按照字段名排序后的 URL 键值对格式字符串 +func BuildSortedQueryString(params map[string]any) string { + // 创建一个字符串切片,用于保存所有的键名 + var keys []string + for key := range params { + keys = append(keys, key) + } + + // 对键名进行 ASCII 字典顺序排序 + sort.Strings(keys) + + // 构建一个 URL 键值对字符串 + var queryStrings []string + for _, key := range keys { + // 拼接 key=value + queryStrings = append(queryStrings, fmt.Sprintf("%s=%v", key, params[key])) + } + + // 使用 & 连接所有的 key=value 对 + return strings.Join(queryStrings, "&") +} + +func Sha1(data string) string { + // 创建一个 SHA-1 哈希对象 + hash := sha1.New() + // 写入数据 + hash.Write([]byte(data)) + // 计算并获取加密后的结果 + hashBytes := hash.Sum(nil) + // 将结果转换为十六进制字符串 + hashString := hex.EncodeToString(hashBytes) + // 打印加密后的 SHA-1 值 + return hashString +}