diff --git a/.gitignore b/.gitignore index b3a0095..2c999f1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ Thumbs.db *.swp .vscode/ .idea/ +.trae/ + bin/ cert/ log diff --git a/api/v1/cmb_cpn.proto b/api/v1/cmb_cpn.proto index ae03540..5ca44e6 100644 --- a/api/v1/cmb_cpn.proto +++ b/api/v1/cmb_cpn.proto @@ -209,7 +209,10 @@ message CmbNotifyRequest { // 扩展字段 string ext = 13 [json_name = "ext"]; string attach = 14 [json_name = "attach"]; + // 招行唯一流水号 + string transactionId = 15 [json_name = "transactionId"]; } + message CmbNotifyReply { // 接口调用返回码,1000 成功,1001 失败 string respCode = 1 [json_name = "respCode"]; @@ -217,6 +220,34 @@ message CmbNotifyReply { string respMsg = 2 [json_name = "respMsg"]; } +message CmbMultiNotifyRequest { + // cmb业务号 + string transactionId = 1 [json_name = "transactionId"]; + // 活动编号 + string activityId = 2 [json_name = "activityId"]; + // 微信券id + string couponId = 3 [json_name = "couponId"]; + // 券领取时间 + string acquiredDate = 4 [json_name = "acquiredDate"]; + // 券状态0:可使用,1:已使用 + string status = 5 [json_name = "status"]; + // String|M 整张券总状态0:可使用,1:已使用 + string couponStatus = 13 [json_name = "couponStatus"]; + // 券核销时间 格式:yyyy-MM-dd HH:mm:ss.sss + string transDate = 6 [json_name = "transDate"]; + // 券核销金额,单位-分 + string transAmount = 7 [json_name = "transAmount"]; + // 券核销支付单号,微信支付系统生成的订单号 + string orderId = 8 [json_name = "orderId"]; + // 优惠券券码 codeNo + string ticket = 9 [json_name = "ticket"]; + // 发码机构号,固定值,掌上生活优惠券系统提供 + string orgNo = 10 [json_name = "orgNo"]; + // 500 标识位 + string attach = 12 [json_name = "attach"]; + // 扩展字段 + string ext = 11 [json_name = "ext"]; +} message EncryptBodyRequest { string encryptBody = 1 [json_name = "encryptBody"]; diff --git a/configs/config.yaml b/configs/config.yaml index 68d4e6c..cca15ea 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -53,6 +53,13 @@ wechat: mchCertificateSerialNumber: "6006B8208815DB5EAC5BF2E783CB9D34082C3772" #商户证书序列号 wechatPayPublicKeyID: "PUB_KEY_ID_0117109533612025031800326400002563" #微信支付公钥ID +wechatSubject: + - mchID: "1100040695" # 证书所属商户 + name: "福建峦峰网络科技有限公司" + mchApiV3Key: "d9af70585b18ae206d981548c766563f" + mchCertificateSerialNumber: "46712853869DB0EDAA9B4DF97DADEECD4CCDC85B" #商户证书序列号 + wechatPayPublicKeyID: "PUB_KEY_ID_0111000406952026032500382251001000" #微信支付公钥ID,有它则为新的“公私钥”模式 + cmb: sm2Prk: "8d39ff3d2559258c163f4510f082727f51531e1953ab203d5ab1ea4a6d94fd73" sm2Puk: "04d827a7dbaaa358ce45b8c7794a7f54819f5c175005a702370e47f135ef6f5f9732758b1474f218419fe9e87f90c28c3b05f08254c651db27df35fae67b77b2e4" # 公钥,给到招行密钥 @@ -66,6 +73,7 @@ cmb: cmbKeyAlias: "SM2_CMBLIFE" orgNo: "LANSEXIONGDI" # 发码机构号,固定值,掌上生活优惠券系统提供 notifyUrl: "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatus.json" # 招行测试回调地址 + multiNotifyUrl: "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatusForMulti.json" # 招行测试多笔立减金回调地址 noticeStartDays: 7 noticeEndDays: 1 @@ -89,6 +97,10 @@ cron: isOpen: true #是否启动 true/false command: "0 */5 * * * ?" #cron表达式,每5分钟执行一次 +tripartite: + qiXing: + appKey: "DrY1zLkOH01q0sN66vrmkdpbWsyb41ho" + rdsMQ: wechatQuery: name: "wechatQuery" @@ -114,6 +126,18 @@ rdsMQ: numWorkers: 1 #协程数量,不配置默认为10 waitTime: 1s #处理完成后等待时间 isOpen: false #是否启动消费 true/false + orderNotifyRetry: + name: "orderNotifyRetry" + retryNum: 1 #重试次数 + numWorkers: 1 #协程数量,不配置默认为10 + waitTime: 1s #处理完成后等待时间 + isOpen: false #是否启动消费 true/false + usedNotify: + name: "usedNotify" + retryNum: 1 #重试次数 + numWorkers: 1 #协程数量,不配置默认为10 + waitTime: 1s #处理完成后等待时间 + isOpen: true #是否启动消费 true/false aliYunSms: accessKeyId: diff --git a/configs/config_test.yaml b/configs/config_test.yaml index 4505078..dbc15ab 100644 --- a/configs/config_test.yaml +++ b/configs/config_test.yaml @@ -66,6 +66,7 @@ cmb: cmbKeyAlias: "SM2_CMBLIFE" orgNo: "LANSEXIONGDI" # 发码机构号,固定值,掌上生活优惠券系统提供 notifyUrl: "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatus.json" # 招行测试回调地址 + multiNotifyUrl: "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatus.json" # 招行测试多笔立减金回调地址 noticeStartDays: 7 noticeEndDays: 1 @@ -103,6 +104,12 @@ rdsMQ: numWorkers: 1 #协程数量,不配置默认为10 waitTime: 1s #处理完成后等待时间 isOpen: false #是否启动消费 true/false + orderNotifyRetry: + name: "orderNotifyRetry" + retryNum: 1 #重试次数 + numWorkers: 1 #协程数量,不配置默认为10 + waitTime: 1s #处理完成后等待时间 + isOpen: false #是否启动消费 true/false aliYunSms: accessKeyId: LTAI5tM1X4HuqUwT8D74qXAH @@ -111,6 +118,10 @@ aliYunSms: signName: 蓝色兄弟 templateWarning: "SMS_489660721" +tripartite: + qiXing: + appKey: "DrY1zLkOH01q0sN66vrmkdpbWsyb41ho" + #配置日志 logs: business: business.log #业务日志路径:如果不写日志,则不配置或配置为空 diff --git a/gorm.sh b/gorm.sh index 52d91e3..5777d2c 100755 --- a/gorm.sh +++ b/gorm.sh @@ -6,7 +6,7 @@ # 3. 在指定目录下创建对应表的CRUD操作代码文件,定义针对该表的常见增删改查以及列表查询、按字段查询等操作方法。 # 设置数据库连接信息,注意密码部分如果包含特殊字符可能需要进行转义处理,这里示例中未做额外处理,需根据实际情况检查 -dsn="root:lansexiongdi6,@tcp(47.97.27.195:3306)/voucher?parseTime=True&loc=Local" +dsn="root:lsxddb123.@tcp(47.108.53.72:3306)/voucher?parseTime=True&loc=Local" # 获取当前脚本所在的绝对路径,即使脚本被在不同目录下调用也能正确定位相关文件和目录 SHELL_FOLDER=$(cd "$(dirname "\$0")" || exit; pwd) echo "$SHELL_FOLDER" diff --git a/internal/biz/bo/multi_notify_data_bo.go b/internal/biz/bo/multi_notify_data_bo.go new file mode 100644 index 0000000..36c5f25 --- /dev/null +++ b/internal/biz/bo/multi_notify_data_bo.go @@ -0,0 +1,27 @@ +package bo + +import ( + "time" + "voucher/internal/biz/vo" +) + +// MultiNotifyDataBo 领域实体Bo结构,字段和模型字段保持一致 +type MultiNotifyDataBo struct { + ID int64 + Source string + IP string + NotifyID string + OrderNo string + OutBizNo string + CouponID string + StockID string + ConsumeAmount int32 + ConsumeTime *time.Time + TransactionID string + EventType string + Status vo.WechatVoucherStatus + OriginalData string + NoticeNum int32 + CreateTime *time.Time + UpdateTime *time.Time +} diff --git a/internal/biz/bo/multi_notify_log_bo.go b/internal/biz/bo/multi_notify_log_bo.go new file mode 100644 index 0000000..5c41e58 --- /dev/null +++ b/internal/biz/bo/multi_notify_log_bo.go @@ -0,0 +1,31 @@ +package bo + +import ( + "time" + "voucher/internal/biz/vo" +) + +// MultiNotifyLogBo 领域实体Bo结构,字段和模型字段保持一致 +type MultiNotifyLogBo struct { + ID int64 + MultiNotifyDataID int64 + OrderNo string + OutBizNo string + CouponID string + ActivityNo string + StockID string + EventType string + Status vo.WechatVoucherStatus + ConsumeAmount int32 + ConsumeTime *time.Time + TransactionID string + RequestURL string + RequestStatus int32 + OriginReq string + Request string + Response string + OrderCreateTime *time.Time + CouponCreateTime *time.Time + CreateTime *time.Time + UpdateTime *time.Time +} diff --git a/internal/biz/bo/wechat_notify_bo.go b/internal/biz/bo/wechat_notify_bo.go new file mode 100644 index 0000000..d8c35ae --- /dev/null +++ b/internal/biz/bo/wechat_notify_bo.go @@ -0,0 +1,116 @@ +package bo + +import ( + "encoding/json" + "fmt" + "github.com/go-playground/validator/v10" + "time" + "voucher/internal/biz/vo" +) + +// ConsumeInformation 定义消费信息结构体 +type ConsumeInformation struct { + ConsumeTime time.Time `json:"consume_time" validate:"required"` // 核销时间 + ConsumeMchid string `json:"consume_mchid"` // 核销商户号 + TransactionID string `json:"transaction_id" validate:"required"` // 微信支付交易单号 + ConsumeAmount int `json:"consume_amount"` // 核销金额(单位:分) // 多笔立减金必须 validate:"required" +} + +// CombineSubOrder 定义合单子单消费信息结构体 +type CombineSubOrder struct { + TransactionID string `json:"transaction_id" validate:"required"` // 合单子单微信支付订单号 + ConsumeAmount int `json:"comsume_amount" validate:"required"` // 子单核销金额,微信文档字段名如此定义 + ConsumeTime time.Time `json:"consume_time" validate:"required"` // 子单核销时间 +} + +// CombineOrderInfo 定义合单订单信息结构体 +type CombineOrderInfo struct { + CombineConsumeAmount int `json:"combine_consume_amount"` // 合单总核销金额 + SubOrders []CombineSubOrder `json:"sub_orders,omitempty"` // 合单子单列表 +} + +// PlainText 定义明文数据结构体 +type PlainText struct { + StockCreatorMchid string `json:"stock_creator_mchid" validate:"required"` + StockID string `json:"stock_id"` + CouponID string `json:"coupon_id"` + CouponName string `json:"coupon_name"` + Description string `json:"description"` + Status vo.WechatVoucherStatus `json:"status"` + CreateTime time.Time `json:"create_time" validate:"required"` + CouponType string `json:"coupon_type"` + NoCash bool `json:"no_cash"` + Singleitem bool `json:"singleitem"` + BusinessType string `json:"business_type"` // 业务类型 + IsCombineOrder bool `json:"is_combine_order,omitempty"` + ConsumeInformation *ConsumeInformation `json:"consume_information,omitempty"` + CombineOrderInfo *CombineOrderInfo `json:"combine_order_info,omitempty"` +} + +type WechatVoucherNotifyBo struct { + ID string `json:"id" validate:"required"` + CreateTime string `json:"create_time"` + ResourceType string `json:"resource_type"` + EventType string `json:"event_type" validate:"required"` + Summary string `json:"summary"` + OriginalType string `json:"original_type"` + AssociatedData string `json:"associated_data"` + PlainText PlainText `json:"plain_text" validate:"required"` +} + +func (d *WechatVoucherNotifyBo) Str() (string, error) { + + b, err := json.Marshal(d) + if err != nil { + return "", fmt.Errorf("json marshal original_data error: %v", err) + } + + return string(b), nil +} + +func (c *WechatVoucherNotifyBo) Validate() error { + + if err := validator.New().Struct(c); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return err + } + } + + return nil +} + +func (c *WechatVoucherNotifyBo) ValidateMultiNotify() error { + if err := c.Validate(); err != nil { + return err + } + + if c.PlainText.IsCombineOrder { + if c.PlainText.CombineOrderInfo == nil { + return fmt.Errorf("合单订单信息不能为空") + } + if len(c.PlainText.CombineOrderInfo.SubOrders) == 0 { + return fmt.Errorf("合单子单不能为空") + } + for _, subOrder := range c.PlainText.CombineOrderInfo.SubOrders { + if subOrder.TransactionID == "" { + return fmt.Errorf("合单子单微信支付订单号不能为空") + } + if subOrder.ConsumeAmount <= 0 { + return fmt.Errorf("合单子单核销金额必须大于0") + } + if subOrder.ConsumeTime.IsZero() { + return fmt.Errorf("合单子单核销时间不能为空") + } + } + return nil + } + + if c.PlainText.ConsumeInformation == nil { + return fmt.Errorf("消费信息不能为空") + } + if c.PlainText.ConsumeInformation.ConsumeAmount <= 0 { + return fmt.Errorf("消费金额必须大于0") + } + + return nil +} diff --git a/internal/biz/bo/wechat_voucher_bo.go b/internal/biz/bo/wechat_voucher_bo.go deleted file mode 100644 index 6c99585..0000000 --- a/internal/biz/bo/wechat_voucher_bo.go +++ /dev/null @@ -1,36 +0,0 @@ -package bo - -import "voucher/internal/biz/vo" - -// ConsumeInformation 定义消费信息结构体 -type ConsumeInformation struct { - ConsumeTime string `json:"consume_time"` - ConsumeMchid string `json:"consume_mchid"` - TransactionID string `json:"transaction_id"` -} - -// PlainText 定义明文数据结构体 -type PlainText struct { - StockCreatorMchid string `json:"stock_creator_mchid"` - StockID string `json:"stock_id"` - CouponID string `json:"coupon_id"` - CouponName string `json:"coupon_name"` - Description string `json:"description"` - Status vo.WechatVoucherStatus `json:"status"` - CreateTime string `json:"create_time"` - CouponType string `json:"coupon_type"` - NoCash bool `json:"no_cash"` - Singleitem bool `json:"singleitem"` - ConsumeInformation ConsumeInformation `json:"consume_information,omitempty"` -} - -type WechatVoucherNotifyBo struct { - ID string `json:"id"` - CreateTime string `json:"create_time"` - ResourceType string `json:"resource_type"` - EventType string `json:"event_type"` - Summary string `json:"summary"` - OriginalType string `json:"original_type"` - AssociatedData string `json:"associated_data"` - PlainText PlainText `json:"plain_text"` -} diff --git a/internal/biz/businesserr/cmb.go b/internal/biz/businesserr/cmb.go new file mode 100644 index 0000000..f84460d --- /dev/null +++ b/internal/biz/businesserr/cmb.go @@ -0,0 +1,60 @@ +package businesserr + +// ThirdErrCode 表示发券接口失败时返回的第三方业务错误码 +// 格式为 tecxxx,用于统一抽象不同支付平台的错误 +type ThirdErrCode string + +// 定义具体的第三方错误码常量(微信/支付宝的都有) +const ( + ThirdErrCodeDefault ThirdErrCode = "tec999" // 默认错误码 + + ThirdErrCodeStockNotEnough ThirdErrCode = "tec001" // 库存不足 + ThirdErrCodeAdvanceFundingNotEnough ThirdErrCode = "tec002" // 垫资不足 + ThirdErrCodeBatchNotStarted ThirdErrCode = "tec003" // 批次未开始 + ThirdErrCodeBatchEnded ThirdErrCode = "tec004" // 批次已结束 + ThirdErrCodeBatchOffline ThirdErrCode = "tec005" // 批次已下线 + ThirdErrCodePaymentPlatformError ThirdErrCode = "tec006" // 微信/支付宝异常 + + ThirdErrCodeUserNotRealNameVerified ThirdErrCode = "tec007" // 用户没有实名认证 + ThirdErrCodeUserNotFound ThirdErrCode = "tec008" // 用户账号不存在 + ThirdErrCodeUserAccountFrozen ThirdErrCode = "tec009" // 用户账户被冻结 + ThirdErrCodeUserHighRiskOrCheater ThirdErrCode = "tec010" // 用户为作弊用户或高风险用户 + ThirdErrCodeUserNoMobileBound ThirdErrCode = "tec011" // 用户没有绑定手机号 + ThirdErrCodeUserAccountAbnormal ThirdErrCode = "tec012" // 用户账号异常(没有明确的原因) + ThirdErrCodeUserParticipationExceeded ThirdErrCode = "tec013" // 用户参与次数超限 + ThirdErrCodeAppIDOpenIDMismatch ThirdErrCode = "tec014" // appid与openid不匹配 + + ThirdErrCodeDailyLimit ThirdErrCode = "tec015" // 超批次当天限额 + ThirdErrCodeCallHigh ThirdErrCode = "tec016" // 调用频率过高 +) + +// CmbAPIError 定义 API 错误结构体 +type CmbAPIError struct { + StatusCode int `json:"status_code"` + ErrorCode ErrCode `json:"error_code"` + Description string `json:"description"` + Hint string `json:"hint"` // 解决方案 + ThirdErrCode ThirdErrCode `json:"third_err_code"` +} + +var CmbAPIPublicErrorMap = map[ErrCode][]CmbAPIError{ + + BATCH_NOT_STARTED: { + { + StatusCode: 400, + ErrorCode: BATCH_NOT_STARTED, + Description: "批次未开始", + Hint: "批次未开始", + ThirdErrCode: ThirdErrCodeDefault, + }, + }, + BATCH_ENDED: { + { + StatusCode: 400, + ErrorCode: BATCH_ENDED, + Description: "批次已结束", + Hint: "批次已结束", + ThirdErrCode: ThirdErrCodeDefault, + }, + }, +} diff --git a/internal/data/wechatrepoimpl/cpn_code.go b/internal/biz/businesserr/cpn.go similarity index 50% rename from internal/data/wechatrepoimpl/cpn_code.go rename to internal/biz/businesserr/cpn.go index 4dfe297..06eeb41 100644 --- a/internal/data/wechatrepoimpl/cpn_code.go +++ b/internal/biz/businesserr/cpn.go @@ -1,19 +1,18 @@ -package wechatrepoimpl +package businesserr import ( "fmt" - "strings" - "github.com/go-kratos/kratos/v2/errors" + "strings" err2 "voucher/api/err" ) -// ErrCode 定义错误码类型 -// https://pay.weixin.qq.com/doc/v3/merchant/4012463767 -type ErrCode string - // 定义错误码常量 +// https://pay.weixin.qq.com/doc/v3/merchant/4012463767 const ( + SYSTEM_ERROR ErrCode = "SYSTEM_ERROR" + SIGN_ERROR ErrCode = "SIGN_ERROR" + APPID_MCHID_NOT_MATCH ErrCode = "APPID_MCHID_NOT_MATCH" INVALID_REQUEST ErrCode = "INVALID_REQUEST" PARAM_ERROR ErrCode = "PARAM_ERROR" @@ -26,196 +25,255 @@ const ( FREQUENCY_LIMITED ErrCode = "FREQUENCY_LIMITED" ) -// APIError 定义 API 错误结构体 -type APIError struct { - StatusCode int `json:"status_code"` - ErrorCode ErrCode `json:"error_code"` - Description string `json:"description"` - Hint string `json:"hint"` -} +const ( + BATCH_NOT_STARTED ErrCode = "BATCH_NOT_STARTED" + BATCH_ENDED ErrCode = "BATCH_ENDED" +) + +// CmbAPIErrorMap 定义错误映射,方便根据错误码获取错误信息 +var CmbAPIErrorMap = map[ErrCode][]CmbAPIError{ + + SYSTEM_ERROR: { + { + StatusCode: 500, + ErrorCode: SYSTEM_ERROR, + Description: "系统异常,请稍后重试", + Hint: "请稍后重试", + ThirdErrCode: ThirdErrCodeDefault, + }, + }, + SIGN_ERROR: { + { + StatusCode: 401, + ErrorCode: SIGN_ERROR, + Description: "验证不通过", + Hint: "请参阅 签名常见问题", + ThirdErrCode: ThirdErrCodeDefault, + }, + }, -// 定义错误映射,方便根据错误码获取错误信息 -var _ = map[ErrCode][]APIError{ APPID_MCHID_NOT_MATCH: { { - StatusCode: 400, - ErrorCode: APPID_MCHID_NOT_MATCH, - Description: "商户号与AppID不匹配", - Hint: "调用接口的商户号需与接口传入的AppID有绑定关系,请参考常见问题Q4", + StatusCode: 400, + ErrorCode: APPID_MCHID_NOT_MATCH, + Description: "商户号与AppID不匹配", + Hint: "调用接口的商户号需与接口传入的AppID有绑定关系,请参考常见问题Q4", + ThirdErrCode: ThirdErrCodeDefault, }, }, INVALID_REQUEST: { { - StatusCode: 400, - ErrorCode: INVALID_REQUEST, - Description: "OpenID与AppID不匹配", - Hint: "OpenID与AppID需有对应关系", + StatusCode: 400, + ErrorCode: INVALID_REQUEST, + Description: "HTTP 请求不符合微信支付 APIv3 接口规则", + Hint: "请参阅 接口规则", + ThirdErrCode: ThirdErrCodeDefault, }, { - StatusCode: 400, - ErrorCode: INVALID_REQUEST, - Description: "非法的商户号", - Hint: "请检查商户号准确性", + StatusCode: 400, + ErrorCode: INVALID_REQUEST, + Description: "OpenID与AppID不匹配", + Hint: "OpenID与AppID需有对应关系", + ThirdErrCode: ThirdErrCodeAppIDOpenIDMismatch, }, { - StatusCode: 400, - ErrorCode: INVALID_REQUEST, - Description: "调用频率过高", - Hint: "请降低API调用频率", + StatusCode: 400, + ErrorCode: INVALID_REQUEST, + Description: "非法的商户号", + Hint: "请检查商户号准确性", + ThirdErrCode: ThirdErrCodeDefault, }, { - StatusCode: 400, - ErrorCode: INVALID_REQUEST, - Description: "活动已结束或未激活", - Hint: "请检查批次状态", + StatusCode: 400, + ErrorCode: INVALID_REQUEST, + Description: "调用频率过高", + Hint: "请降低API调用频率", + ThirdErrCode: ThirdErrCodeCallHigh, }, { - StatusCode: 400, - ErrorCode: INVALID_REQUEST, - Description: "批次信息获取失败,请确认参数是否有误", - Hint: "请检查创建商户号与批次号的对应关系", + StatusCode: 400, + ErrorCode: INVALID_REQUEST, + Description: "活动已结束或未激活", + Hint: "请检查批次状态", + ThirdErrCode: ThirdErrCodeBatchEnded, //前置判断时间处理一下 + }, + { + StatusCode: 400, + ErrorCode: INVALID_REQUEST, + Description: "批次信息获取失败,请确认参数是否有误", + Hint: "请检查创建商户号与批次号的对应关系", + ThirdErrCode: ThirdErrCodeDefault, }, }, PARAM_ERROR: { { - StatusCode: 400, - ErrorCode: PARAM_ERROR, - Description: "AppID必填", - Hint: "请输入AppID", + StatusCode: 400, + ErrorCode: PARAM_ERROR, + Description: "参数错误", + Hint: "请根据错误提示正确传入参数", + ThirdErrCode: ThirdErrCodeDefault, + }, + + { + StatusCode: 400, + ErrorCode: PARAM_ERROR, + Description: "AppID必填", + Hint: "请输入AppID", + ThirdErrCode: ThirdErrCodeDefault, }, { - StatusCode: 400, - ErrorCode: PARAM_ERROR, - Description: "OpenID必填", - Hint: "请输入OpenID", + StatusCode: 400, + ErrorCode: PARAM_ERROR, + Description: "OpenID必填", + Hint: "请输入OpenID", + ThirdErrCode: ThirdErrCodeDefault, }, { - StatusCode: 400, - ErrorCode: PARAM_ERROR, - Description: "批次号必填", - Hint: "请输入批次号", + StatusCode: 400, + ErrorCode: PARAM_ERROR, + Description: "批次号必填", + Hint: "请输入批次号", + ThirdErrCode: ThirdErrCodeDefault, }, { - StatusCode: 400, - ErrorCode: PARAM_ERROR, - Description: "商户号必填", - Hint: "请输入商户号", + StatusCode: 400, + ErrorCode: PARAM_ERROR, + Description: "商户号必填", + Hint: "请输入商户号", + ThirdErrCode: ThirdErrCodeDefault, }, { - StatusCode: 400, - ErrorCode: PARAM_ERROR, - Description: "非法的批次状态", - Hint: "请检查批次状态,仅支持发放状态为“运营中”的代金券批次", + StatusCode: 400, + ErrorCode: PARAM_ERROR, + Description: "非法的批次状态", + Hint: "请检查批次状态,仅支持发放状态为“运营中”的代金券批次", + ThirdErrCode: ThirdErrCodeDefault, }, }, MCH_NOT_EXISTS: { { - StatusCode: 403, - ErrorCode: MCH_NOT_EXISTS, - Description: "商户号不合法", - Hint: "请检查商户号准确性", + StatusCode: 403, + ErrorCode: MCH_NOT_EXISTS, + Description: "商户号不合法", + Hint: "请检查商户号准确性", + ThirdErrCode: ThirdErrCodeDefault, }, }, NOT_ENOUGH: { { - StatusCode: 403, - ErrorCode: NOT_ENOUGH, - Description: "批次预算不足", - Hint: "批次预算已发放完,请补充批次预算", + StatusCode: 403, + ErrorCode: NOT_ENOUGH, + Description: "批次预算不足", + Hint: "批次预算已发放完,请补充批次预算", + ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough, }, { - StatusCode: 403, - ErrorCode: NOT_ENOUGH, - Description: "发券超过单天限额", - Hint: "已超过该批次设置的单天发放限制额度,无法发放", + StatusCode: 403, + ErrorCode: NOT_ENOUGH, + Description: "发券超过单天限额", + Hint: "已超过该批次设置的单天发放限制额度,无法发放", + ThirdErrCode: ThirdErrCodeDailyLimit, }, { - StatusCode: 403, - ErrorCode: NOT_ENOUGH, - Description: "账户余额不足,请充值", - Hint: "商户号余额不足,无法继续发券,请充值", + StatusCode: 403, + ErrorCode: NOT_ENOUGH, + Description: "账户余额不足,请充值", + Hint: "商户号余额不足,无法继续发券,请充值", + ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough, }, { - StatusCode: 403, - ErrorCode: NOT_ENOUGH, - Description: "批次预算耗尽", - Hint: "该批次的预算已经耗尽", + StatusCode: 403, + ErrorCode: NOT_ENOUGH, + Description: "批次预算耗尽", + Hint: "该批次的预算已经耗尽", + ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough, }, }, REQUEST_BLOCKED: { { - StatusCode: 403, - ErrorCode: REQUEST_BLOCKED, - Description: "商户无权发券", - Hint: "该批次不支持其他商户发放,请参考常见问题Q1", + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "商户无权发券", + Hint: "该批次不支持其他商户发放,请参考常见问题Q1", + ThirdErrCode: ThirdErrCodeDefault, }, { - StatusCode: 403, - ErrorCode: REQUEST_BLOCKED, - Description: "批次不支持跨商户发券", - Hint: "该批次不支持其他商户发放,请参考常见问题Q1", + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "批次不支持跨商户发券", + Hint: "该批次不支持其他商户发放,请参考常见问题Q1", + ThirdErrCode: ThirdErrCodeDefault, }, { - StatusCode: 403, - ErrorCode: REQUEST_BLOCKED, - Description: "用户被限领拦截", - Hint: "该用户已达到该批次的领取上限,请参考常见问题Q6", + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "用户被限领拦截", + Hint: "该用户已达到该批次的领取上限,请参考常见问题Q6", + ThirdErrCode: ThirdErrCodeUserParticipationExceeded, }, { - StatusCode: 403, - ErrorCode: REQUEST_BLOCKED, - Description: "不能在API渠道发放", - Hint: "请检查批次信息,仅支持发放微信支付代金券,不支持发放立减与折扣", + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "不能在API渠道发放", + Hint: "请检查批次信息,仅支持发放微信支付代金券,不支持发放立减与折扣", + ThirdErrCode: ThirdErrCodeDefault, }, { - StatusCode: 403, - ErrorCode: REQUEST_BLOCKED, - Description: "不支持指定面额发券", - Hint: "仅在发券时指定面额及门槛的场景才生效,常规发券场景请勿传入该信息", + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "不支持指定面额发券", + Hint: "仅在发券时指定面额及门槛的场景才生效,常规发券场景请勿传入该信息", + ThirdErrCode: ThirdErrCodeDefault, }, { - StatusCode: 403, - ErrorCode: REQUEST_BLOCKED, - Description: "仅在广告场景下发放批次", - Hint: "该批次已在朋友圈广告发放,不支持在其他渠道发放", + StatusCode: 403, + ErrorCode: REQUEST_BLOCKED, + Description: "仅在广告场景下发放批次", + Hint: "该批次已在朋友圈广告发放,不支持在其他渠道发放", + ThirdErrCode: ThirdErrCodeDefault, }, }, RULE_LIMIT: { { - StatusCode: 403, - ErrorCode: RULE_LIMIT, - Description: "用户已达最大领券次数", - Hint: "该用户已达到该批次的领取上限,请参考常见问题Q6", + StatusCode: 403, + ErrorCode: RULE_LIMIT, + Description: "用户已达最大领券次数", + Hint: "该用户已达到该批次的领取上限,请参考常见问题Q6", + ThirdErrCode: ThirdErrCodeUserParticipationExceeded, }, { - StatusCode: 403, - ErrorCode: RULE_LIMIT, - Description: "被自然人规则拦截", - Hint: "该自然人已达到该批次的领取上限,请参考常见问题Q6", + StatusCode: 403, + ErrorCode: RULE_LIMIT, + Description: "被自然人规则拦截", + Hint: "该自然人已达到该批次的领取上限,请参考常见问题Q6", + ThirdErrCode: ThirdErrCodeUserAccountAbnormal, }, }, USER_ACCOUNT_ABNORMAL: { { - StatusCode: 403, - ErrorCode: USER_ACCOUNT_ABNORMAL, - Description: "用户非法", - Hint: "用户命中微信支付风控模型,请参考常见问题Q5", + StatusCode: 403, + ErrorCode: USER_ACCOUNT_ABNORMAL, + Description: "用户非法", + Hint: "用户命中微信支付风控模型,请参考常见问题Q5", + ThirdErrCode: ThirdErrCodeUserAccountFrozen, }, }, RESOURCE_NOT_EXISTS: { { - StatusCode: 404, - ErrorCode: RESOURCE_NOT_EXISTS, - Description: "批次不存在", - Hint: "请检查批次及制券商户号信息", + StatusCode: 404, + ErrorCode: RESOURCE_NOT_EXISTS, + Description: "批次不存在", + Hint: "请检查批次及制券商户号信息", + ThirdErrCode: ThirdErrCodeDefault, }, }, FREQUENCY_LIMITED: { { - StatusCode: 429, - ErrorCode: FREQUENCY_LIMITED, - Description: "当前请求人数过多,请稍后重试", - Hint: "请降低API调用频率", + StatusCode: 429, + ErrorCode: FREQUENCY_LIMITED, + Description: "当前请求人数过多,请稍后重试", + Hint: "请降低API调用频率", + ThirdErrCode: ThirdErrCodeCallHigh, }, }, } @@ -284,13 +342,13 @@ var WechatError = map[string]*errors.Error{ ErrorWechatAccountFail: err2.ErrorWechatAccountFail(ErrorWechatAccountFail), } -type ErrBody struct { +type WechatErrBody struct { Code ErrCode `json:"Code"` Message string `json:"Message"` } // GetWechatError 根据错误描述获取具体的错误处理 -func (s ErrBody) GetWechatError() *errors.Error { +func (s WechatErrBody) GetWechatError() *errors.Error { lowerDesc := strings.ToLower(s.Message) for desc, err := range WechatError { if strings.ToLower(desc) == lowerDesc { diff --git a/internal/biz/businesserr/cpn_multi.go b/internal/biz/businesserr/cpn_multi.go new file mode 100644 index 0000000..182f5ac --- /dev/null +++ b/internal/biz/businesserr/cpn_multi.go @@ -0,0 +1,197 @@ +package businesserr + +// 公共 & 业务错误码常量(统一定义,前缀 MULTI_) +// https://pay.weixin.qq.com/doc/v3/merchant/4012463767 +const ( + MULTI_PARAM_ERROR ErrCode = "PARAM_ERROR" // 参数错误(如缺少必填字段) + MULTI_INVALID_REQUEST ErrCode = "INVALID_REQUEST" // 非法请求(如 OpenID 与 AppID 不匹配、批次状态异常等) + MULTI_SIGN_ERROR ErrCode = "SIGN_ERROR" // 签名验证失败 + MULTI_SYSTEM_ERROR ErrCode = "SYSTEM_ERROR" // 系统异常 + MULTI_APPID_MCHID_NOT_MATCH ErrCode = "APPID_MCHID_NOT_MATCH" // 商户号与 AppID 不匹配 + MULTI_MCH_NOT_EXISTS ErrCode = "MCH_NOT_EXISTS" // 商户号不合法 + MULTI_NOT_ENOUGH ErrCode = "NOT_ENOUGH" // 资源不足(预算/余额/限额耗尽) + MULTI_REQUEST_BLOCKED ErrCode = "REQUEST_BLOCKED" // 请求被拦截(跨商户、渠道限制等) + MULTI_RULE_LIMIT ErrCode = "RULE_LIMIT" // 用户或自然人达到领取上限 + MULTI_USER_ACCOUNT_ABNORMAL ErrCode = "USER_ACCOUNT_ABNORMAL" // 用户账号异常(风控、未实名等) + MULTI_RESOURCE_NOT_EXISTS ErrCode = "RESOURCE_NOT_EXISTS" // 批次不存在 +) + +// MULTIAPIErrorMap 定义错误映射,方便根据错误码获取所有可能的错误场景 +var CmbMULTIAPIErrorMap = map[ErrCode][]CmbAPIError{ + MULTI_SYSTEM_ERROR: { + { + StatusCode: 500, + ErrorCode: MULTI_SYSTEM_ERROR, + Description: "系统异常,请稍后重试", + Hint: "请稍后重试", + ThirdErrCode: ThirdErrCodeDefault, + }, + }, + MULTI_SIGN_ERROR: { + { + StatusCode: 401, + ErrorCode: MULTI_SIGN_ERROR, + Description: "验证不通过", + Hint: "请参阅 签名常见问题", + ThirdErrCode: ThirdErrCodeDefault, + }, + }, + MULTI_APPID_MCHID_NOT_MATCH: { + { + StatusCode: 400, + ErrorCode: MULTI_APPID_MCHID_NOT_MATCH, + Description: "商户号与AppID不匹配", + Hint: "调用接口的商户号需与接口传入的AppID有绑定关系,请参考常见问题Q4", + ThirdErrCode: ThirdErrCodeDefault, + }, + }, + MULTI_INVALID_REQUEST: { + { + StatusCode: 400, + ErrorCode: MULTI_INVALID_REQUEST, + Description: "HTTP 请求不符合微信支付 APIv3 接口规则", + Hint: "请参阅 接口规则", + ThirdErrCode: ThirdErrCodeDefault, + }, + { + StatusCode: 400, + ErrorCode: MULTI_INVALID_REQUEST, + Description: "非法的商户号", + Hint: "请检查商户号准确性", + ThirdErrCode: ThirdErrCodeDefault, + }, + { + StatusCode: 400, + ErrorCode: MULTI_INVALID_REQUEST, + Description: "OpenID与AppID不匹配", + Hint: "OpenID与AppID需有对应关系", + ThirdErrCode: ThirdErrCodeAppIDOpenIDMismatch, + }, + }, + MULTI_PARAM_ERROR: { + { + StatusCode: 400, + ErrorCode: MULTI_PARAM_ERROR, + Description: "参数错误", + Hint: "请根据错误提示正确传入参数", + ThirdErrCode: ThirdErrCodeDefault, + }, + { + StatusCode: 400, + ErrorCode: MULTI_PARAM_ERROR, + Description: "AppID必填", + Hint: "请输入AppID", + ThirdErrCode: ThirdErrCodeDefault, + }, + { + StatusCode: 400, + ErrorCode: MULTI_PARAM_ERROR, + Description: "OpenID必填", + Hint: "请输入OpenID", + ThirdErrCode: ThirdErrCodeDefault, + }, + { + StatusCode: 400, + ErrorCode: MULTI_PARAM_ERROR, + Description: "批次号必填", + Hint: "请输入批次号", + ThirdErrCode: ThirdErrCodeDefault, + }, + { + StatusCode: 400, + ErrorCode: MULTI_PARAM_ERROR, + Description: "商户号必填", + Hint: "请输入商户号", + ThirdErrCode: ThirdErrCodeDefault, + }, + }, + MULTI_MCH_NOT_EXISTS: { + { + StatusCode: 403, + ErrorCode: MULTI_MCH_NOT_EXISTS, + Description: "商户号不合法", + Hint: "请检查商户号准确性", + ThirdErrCode: ThirdErrCodeDefault, + }, + }, + MULTI_NOT_ENOUGH: { + { + StatusCode: 403, + ErrorCode: MULTI_NOT_ENOUGH, + Description: "批次预算耗尽", + Hint: "该批次的预算已经耗尽", + ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough, + }, + { + StatusCode: 403, + ErrorCode: MULTI_NOT_ENOUGH, + Description: "账户余额不足,请充值", + Hint: "商户号余额不足,无法继续发券,请充值", + ThirdErrCode: ThirdErrCodeAdvanceFundingNotEnough, + }, + { + StatusCode: 403, + ErrorCode: MULTI_NOT_ENOUGH, + Description: "发券超过单天限额", + Hint: "已超过该批次设置的单天发放限制额度,无法发放", + ThirdErrCode: ThirdErrCodeDailyLimit, + }, + }, + MULTI_REQUEST_BLOCKED: { + { + StatusCode: 403, + ErrorCode: MULTI_REQUEST_BLOCKED, + Description: "参数错误,请检查批次参数", + Hint: "活动未开始或已结束", + ThirdErrCode: ThirdErrCodeBatchNotStarted, // 时间前置判断一下 + }, + { + StatusCode: 403, + ErrorCode: MULTI_REQUEST_BLOCKED, + Description: "仅在广告场景下发放批次", + Hint: "该批次已在朋友圈广告发放,不支持在其他渠道发放", + ThirdErrCode: ThirdErrCodeDefault, + }, + { + StatusCode: 403, + ErrorCode: MULTI_REQUEST_BLOCKED, + Description: "商户号收款功能受限,无法发券", + Hint: "商户号收款功能受限", + ThirdErrCode: ThirdErrCodeDefault, + }, + { + StatusCode: 403, + ErrorCode: MULTI_REQUEST_BLOCKED, + Description: "批次不支持跨商户发券", + Hint: "该批次不支持其他商户发放", + ThirdErrCode: ThirdErrCodeDefault, + }, + }, + MULTI_RULE_LIMIT: { + { + StatusCode: 403, + ErrorCode: MULTI_RULE_LIMIT, + Description: "用户已达最大领券次数", + Hint: "该用户已达到该批次的领取上限", + ThirdErrCode: ThirdErrCodeUserParticipationExceeded, + }, + }, + MULTI_USER_ACCOUNT_ABNORMAL: { + { + StatusCode: 403, + ErrorCode: MULTI_USER_ACCOUNT_ABNORMAL, + Description: "用户未实名", + Hint: "该用户无实名信息,无法领券。商家可联系微信支付或让用户联系微信支付客服处理。", + ThirdErrCode: ThirdErrCodeUserNotRealNameVerified, + }, + }, + MULTI_RESOURCE_NOT_EXISTS: { + { + StatusCode: 404, + ErrorCode: MULTI_RESOURCE_NOT_EXISTS, + Description: "批次不存在", + Hint: "请检查批次及制券商户号信息", + ThirdErrCode: ThirdErrCodeDefault, + }, + }, +} diff --git a/internal/biz/businesserr/err.go b/internal/biz/businesserr/err.go new file mode 100644 index 0000000..dcc3558 --- /dev/null +++ b/internal/biz/businesserr/err.go @@ -0,0 +1,83 @@ +package businesserr + +// ErrCode 定义错误码类型 +type ErrCode string + +type BusinessErr struct { + Code ErrCode `json:"code"` + Message string `json:"message"` +} + +func (e *BusinessErr) Error() string { + return e.Message +} + +var ( + BatchNotSetStartedError = &BusinessErr{Code: ErrCode("400"), Message: "批次开始时间未设置"} + BatchSetEndedError = &BusinessErr{Code: ErrCode("400"), Message: "批次已结束时间未设置"} + + BatchNotStartedError = &BusinessErr{Code: ErrCode("400"), Message: "批次未开始"} + BatchEndedError = &BusinessErr{Code: ErrCode("400"), Message: "批次已结束"} +) + +func (err *BusinessErr) HandleThirdErrCode() CmbAPIError { + + errCode := err.Code + + emp, ok := CmbAPIPublicErrorMap[errCode] + if ok { + for _, e := range emp { + if e.Description == err.Message { + return e + } + } + } + + em, ok := CmbAPIErrorMap[errCode] + if ok { + for _, e := range em { + if e.Description == err.Message { + return e + } + } + } + + return CmbAPIError{ + StatusCode: 499, + ErrorCode: errCode, + Description: err.Message, + Hint: "", + ThirdErrCode: ThirdErrCodeDefault, + } +} + +func (err *BusinessErr) HandleMULTIThirdErrCode() CmbAPIError { + + errCode := err.Code + + emp, ok := CmbAPIPublicErrorMap[errCode] + if ok { + for _, e := range emp { + if e.Description == err.Message { + return e + } + } + } + + em2, ok := CmbMULTIAPIErrorMap[errCode] + if ok { + for _, e := range em2 { + if e.Description == err.Message { + return e + } + } + } + + return CmbAPIError{ + StatusCode: 499, + ErrorCode: errCode, + Description: err.Message, + Hint: "", + ThirdErrCode: ThirdErrCodeDefault, + } +} diff --git a/internal/biz/cmb/notify.go b/internal/biz/cmb/notify.go index 4c7d80e..1a0e54f 100644 --- a/internal/biz/cmb/notify.go +++ b/internal/biz/cmb/notify.go @@ -63,12 +63,13 @@ func (v *Cmb) bizContent(_ context.Context, order *bo.OrderBo, orderNotify *bo.O } req := &v1.CmbNotifyRequest{ - Ticket: orderNotify.OrderNo, - Status: cmbStatus.GetValue(), - TransDate: "", // 格式yyyy-mm-dd hh:mm:ss.sss - OrgNo: v.bc.Cmb.OrgNo, - Attach: order.Attach, - Ext: "", + Ticket: orderNotify.OrderNo, + Status: cmbStatus.GetValue(), + TransDate: "", // 格式yyyy-mm-dd hh:mm:ss.sss + OrgNo: v.bc.Cmb.OrgNo, + Attach: order.Attach, + Ext: "", + TransactionId: order.OutBizNo, // 招行订单号 } if cmbStatus == vo.CmbStatusUse { diff --git a/internal/biz/cron_notice.go b/internal/biz/cron_notice.go index 279e954..ec00c6a 100644 --- a/internal/biz/cron_notice.go +++ b/internal/biz/cron_notice.go @@ -86,10 +86,10 @@ func (this *VoucherBiz) timeSliceQuery(ctx context.Context, startTime, endTime t log.Warnf("订单定时通知,开始处理,按每两个小时分片处理,范围:%s到%s", startTime.Format(time.DateTime), endTime.Format(time.DateTime)) - duration := 2 * time.Hour + duration := 6 * time.Hour eg := new(errgroup.Group) - eg.SetLimit(10) + eg.SetLimit(8) for start := startTime; start.Before(endTime); start = start.Add(duration) { diff --git a/internal/biz/do/rds_mq.go b/internal/biz/do/rds_mq.go index 6daf51e..4a587fc 100644 --- a/internal/biz/do/rds_mq.go +++ b/internal/biz/do/rds_mq.go @@ -1,5 +1,14 @@ package do +type OrderNotifyRetry struct { + ProductNo string `json:"product_no"` + BatchNo string `json:"batch_no"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + OrderNo string `json:"order_no"` + OutBizNo string `json:"out_biz_no"` +} + type WechatQuery struct { ProductNo string `json:"product_no"` BatchNo string `json:"batch_no"` @@ -16,3 +25,23 @@ type RdsWechatQuery struct { GoNum int `json:"go_num"` // 并发数 TimeSliceHours int64 `json:"time_slice_hours"` // 时间片"小时" } + +type WechatUsedQuery struct { + ProductNo string `json:"product_no"` + BatchNo string `json:"batch_no"` + OrderNo string `json:"order_no"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` +} + +type RetryQueryNotice struct { + ProductNo string `json:"product_no"` + + ReceiveSuccessStartTime string `json:"receive_success_start_time"` + ReceiveSuccessEndTime string `json:"receive_success_end_time"` + + OrderNos []string `json:"order_nos"` + BatchNos []string `json:"batch_nos"` + OutBizNos []string `json:"out_biz_nos"` + VoucherNos []string `json:"voucher_nos"` +} diff --git a/internal/biz/multi.go b/internal/biz/multi.go new file mode 100644 index 0000000..598a92c --- /dev/null +++ b/internal/biz/multi.go @@ -0,0 +1,449 @@ +package biz + +import ( + "context" + "encoding/json" + "errors" + "fmt" + v1 "voucher/api/v1" + "voucher/internal/biz/bo" + "voucher/internal/biz/cmb" + "voucher/internal/biz/mixrepos" + "voucher/internal/biz/repo" + "voucher/internal/biz/vo" + "voucher/internal/conf" + "voucher/internal/data" + "voucher/internal/pkg/lock" + + "github.com/go-kratos/kratos/v2/log" + "gorm.io/gorm" +) + +type MultiBiz struct { + bc *conf.Bootstrap + rdb *data.Rdb + Cmb *cmb.Cmb + ProductRepo repo.ProductRepo + OrderRepo repo.OrderRepo + MultiNotifyDataRepo repo.MultiNotifyDataRepo + MultiNotifyLogRepo repo.MultiNotifyLogRepo + CmbMixRepo mixrepos.CmbMixRepo +} + +func NewMultiBiz( + bc *conf.Bootstrap, + rdb *data.Rdb, + cmb *cmb.Cmb, + productRepo repo.ProductRepo, + orderRepo repo.OrderRepo, + multiNotifyDataRepo repo.MultiNotifyDataRepo, + multiNotifyLogRepo repo.MultiNotifyLogRepo, + cmbMixRepo mixrepos.CmbMixRepo, +) *MultiBiz { + return &MultiBiz{ + bc: bc, + rdb: rdb, + Cmb: cmb, + ProductRepo: productRepo, + OrderRepo: orderRepo, + MultiNotifyDataRepo: multiNotifyDataRepo, + MultiNotifyLogRepo: multiNotifyLogRepo, + CmbMixRepo: cmbMixRepo, + } +} + +func (biz *MultiBiz) Notify(ctx context.Context, ip, source string, req *bo.WechatVoucherNotifyBo) error { + + // 商品数据量较少,先查询商品是否存在,过滤多余的通知信息 + _, err := biz.ProductRepo.GetByMchStockId(ctx, req.PlainText.StockCreatorMchid, req.PlainText.StockID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return fmt.Errorf("商品查询错误 error: %w", err) + } + + cl := vo.MultiNotifyLockKey.BuildCache([]string{ + source, + req.PlainText.StockID, + req.PlainText.CouponID, + }) + + return lock.NewMutex(biz.rdb.Rdb, cl.TTL).Lock(ctx, cl.Key, func(ctx context.Context) error { + + if err = req.ValidateMultiNotify(); err != nil { + return fmt.Errorf("multi validate req error: %v", err) + } + + order, err := biz.order(ctx, req) + if err != nil { + return err + } + + if order.ActivityId == "" { + return fmt.Errorf("批次活动ID为空,不是多笔立减金,请检查") + } + + if err = biz.Run(ctx, ip, source, req, order); err != nil { + return err + } + + return nil + }) + +} + +func (biz *MultiBiz) order(ctx context.Context, req *bo.WechatVoucherNotifyBo) (*bo.OrderBo, error) { + + order, err := biz.OrderRepo.GetByCouponId(ctx, req.PlainText.StockCreatorMchid, req.PlainText.StockID, req.PlainText.CouponID) + if err != nil { + return nil, fmt.Errorf("订单查询错误 error: %w", err) + } + + return order, nil +} + +func (biz *MultiBiz) Run(ctx context.Context, ip, source string, req *bo.WechatVoucherNotifyBo, order *bo.OrderBo) error { + + if order.ActivityId == "" { + return fmt.Errorf("批次活动ID为空,不是多笔立减金,请检查") + } + + mnd, err := biz.MultiNotifyDataRepo.GetByNotifyID(ctx, source, req.ID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("查询通知数据错误 error: %v", err) + } + + if mnd != nil { + if !req.PlainText.IsCombineOrder && mnd.NoticeNum > 0 { + log.Warnf("[%s] multi notify log already exists,req:%+v", source, req) + return nil + } + } else { + mnd, err = biz.mndCreate(ctx, ip, source, req, order) + if err != nil { + return fmt.Errorf("创建通知数据错误 error: %v", err) + } + } + + return biz.run(ctx, req, mnd, order) +} + +func (biz *MultiBiz) RetryRunByMultiNotifyDataId(ctx context.Context, multiNotifyDataId int64) error { + + mnd, err := biz.MultiNotifyDataRepo.GetByID(ctx, multiNotifyDataId) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("查询通知数据错误 error: %v", err) + } + + order, err := biz.OrderRepo.GetByOrderNo(ctx, mnd.OrderNo) + if err != nil { + return fmt.Errorf("订单查询错误 error: %v", err) + } + + var req *bo.WechatVoucherNotifyBo + if err = json.Unmarshal([]byte(mnd.OriginalData), &req); err != nil { + return fmt.Errorf("通知数据 json unmarshal 错误 error: %v", err) + } + + return biz.run(ctx, req, mnd, order) +} + +func (biz *MultiBiz) run(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) error { + if req.PlainText.IsCombineOrder { + return biz.runCombine(ctx, req, mnd, order) + } + + return biz.runSingle(ctx, req, mnd, order) +} + +func (biz *MultiBiz) runSingle(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) error { + // 如果核销金额为空,不再推送下游 + if mnd.ConsumeAmount == 0 { + log.Warnf("[%s] multi notify log consume amount is 0,req:%+v", mnd.NotifyID, req) + return nil + } + + nl, request, err := biz.nlCreate(ctx, req, mnd, order) + if err != nil { + return fmt.Errorf("创建通知日志错误 error: %v", err) + } + + if err = biz.Request(ctx, mnd, nl, request); err != nil { + return fmt.Errorf("请求错误 error: %v", err) + } + + return biz.updateOrderStatus(ctx, req, order) +} + +func (biz *MultiBiz) runCombine(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) error { + for _, subOrder := range req.PlainText.CombineOrderInfo.SubOrders { + exists, err := biz.MultiNotifyLogRepo.ExistsSuccessByDataIDAndTransactionID(ctx, mnd.ID, subOrder.TransactionID) + if err != nil { + return fmt.Errorf("查询合单子单通知记录错误 error: %v", err) + } + if exists { + log.Warnf("[%s] combine sub order already notified,transaction_id:%s", mnd.NotifyID, subOrder.TransactionID) + continue + } + + nl, request, err := biz.nlCreateBySubOrder(ctx, req, mnd, order, subOrder) + if err != nil { + return fmt.Errorf("创建合单子单通知日志错误 error: %v", err) + } + + if err = biz.Request(ctx, mnd, nl, request); err != nil { + return fmt.Errorf("合单子单请求错误 error: %v", err) + } + } + + return biz.updateOrderStatus(ctx, req, order) +} + +func (biz *MultiBiz) updateOrderStatus(ctx context.Context, req *bo.WechatVoucherNotifyBo, order *bo.OrderBo) error { + consumeTime := req.PlainText.ConsumeInformation.ConsumeTime + + if req.PlainText.Status.IsUsed() { + + if order.Status.IsUse() { + if err := biz.OrderRepo.MultiOverUsed(ctx, order.ID, consumeTime, "再次核销完成"); err != nil { + return fmt.Errorf("订单再次核销完成修改发生错误 error: %v", err) + } + } else { + if err := biz.OrderRepo.MultiOverUsed(ctx, order.ID, consumeTime, "核销完成"); err != nil { + return fmt.Errorf("订单核销完成修改发生错误 error: %v", err) + } + } + + } else { + if err := biz.OrderRepo.MultiLastUsed(ctx, order.ID, consumeTime); err != nil { + return fmt.Errorf("订单核销修改发生错误 error: %v", err) + } + } + + return nil +} + +func (biz *MultiBiz) mndCreate(ctx context.Context, ip, source string, req *bo.WechatVoucherNotifyBo, order *bo.OrderBo) (*bo.MultiNotifyDataBo, error) { + + originalData, err := req.Str() + if err != nil { + return nil, fmt.Errorf("通知数据 json str 错误 error: %v", err) + } + + consumeAmount := int32(req.PlainText.ConsumeInformation.ConsumeAmount) + consumeTime := &req.PlainText.ConsumeInformation.ConsumeTime + transactionID := req.PlainText.ConsumeInformation.TransactionID + if req.PlainText.IsCombineOrder { + consumeAmount = int32(req.PlainText.CombineOrderInfo.CombineConsumeAmount) + } + + return biz.MultiNotifyDataRepo.Create(ctx, &bo.MultiNotifyDataBo{ + Source: source, + IP: ip, + NotifyID: req.ID, + OrderNo: order.OrderNo, + OutBizNo: order.OutBizNo, + CouponID: req.PlainText.CouponID, + StockID: req.PlainText.StockID, + ConsumeAmount: consumeAmount, + ConsumeTime: consumeTime, + TransactionID: transactionID, + EventType: req.EventType, + Status: req.PlainText.Status, + OriginalData: originalData, + }) +} + +func (biz *MultiBiz) nlCreateBySubOrder(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo, subOrder bo.CombineSubOrder) (*bo.MultiNotifyLogBo, *v1.CmbRequest, error) { + if biz.bc.Cmb.MultiNotifyUrl == "" { + return nil, nil, fmt.Errorf("CMB多笔立减金通知地址为空") + } + + consumeTime := subOrder.ConsumeTime + nl := &bo.MultiNotifyLogBo{ + MultiNotifyDataID: mnd.ID, + OrderNo: mnd.OrderNo, + OutBizNo: mnd.OutBizNo, + CouponID: mnd.CouponID, + ActivityNo: order.ProductNo, + StockID: mnd.StockID, + EventType: mnd.EventType, + Status: req.PlainText.Status, + ConsumeAmount: int32(subOrder.ConsumeAmount), + ConsumeTime: &consumeTime, + TransactionID: subOrder.TransactionID, + RequestURL: biz.bc.Cmb.MultiNotifyUrl, + RequestStatus: vo.MultiNotifyLogStatusWait.GetValue(), + OrderCreateTime: order.CreateTime, + CouponCreateTime: &req.PlainText.CreateTime, + } + + request, cmbRequestBo, err := biz.GetRequest(ctx, nl, order) + if err != nil { + return nil, nil, err + } + + b, _ := json.Marshal(request) + nl.OriginReq = cmbRequestBo.BizContent + nl.Request = string(b) + + res, err := biz.MultiNotifyLogRepo.Create(ctx, nl) + if err != nil { + return nil, nil, fmt.Errorf("创建通知日志错误 error: %v", err) + } + res.ConsumeTime = nl.ConsumeTime + + return res, request, nil +} + +func (biz *MultiBiz) nlCreate(ctx context.Context, req *bo.WechatVoucherNotifyBo, mnd *bo.MultiNotifyDataBo, order *bo.OrderBo) (*bo.MultiNotifyLogBo, *v1.CmbRequest, error) { + + if biz.bc.Cmb.MultiNotifyUrl == "" { + return nil, nil, fmt.Errorf("CMB多笔立减金通知地址为空") + } + + nl := &bo.MultiNotifyLogBo{ + MultiNotifyDataID: mnd.ID, + OrderNo: mnd.OrderNo, + OutBizNo: mnd.OutBizNo, + CouponID: mnd.CouponID, + ActivityNo: order.ProductNo, + StockID: mnd.StockID, + EventType: mnd.EventType, + Status: req.PlainText.Status, + ConsumeAmount: mnd.ConsumeAmount, + ConsumeTime: mnd.ConsumeTime, + TransactionID: req.PlainText.ConsumeInformation.TransactionID, + //RequestURL: order.NotifyUrl, + RequestURL: biz.bc.Cmb.MultiNotifyUrl, + RequestStatus: vo.MultiNotifyLogStatusWait.GetValue(), + OrderCreateTime: order.CreateTime, + CouponCreateTime: &req.PlainText.CreateTime, + } + + request, cmbRequestBo, err := biz.GetRequest(ctx, nl, order) + if err != nil { + return nil, nil, err + } + + b, _ := json.Marshal(request) + nl.OriginReq = cmbRequestBo.BizContent + nl.Request = string(b) + + res, err := biz.MultiNotifyLogRepo.Create(ctx, nl) + if err != nil { + return nil, nil, fmt.Errorf("创建通知日志错误 error: %v", err) + } + // 创建出来的核销时间精度有差距,重新赋值返回出去 + res.ConsumeTime = mnd.ConsumeTime + + return res, request, nil +} + +func (biz *MultiBiz) bizContent(nl *bo.MultiNotifyLogBo, order *bo.OrderBo) (string, error) { + + req := &v1.CmbMultiNotifyRequest{ + TransactionId: nl.OutBizNo, // cmb业务号 + ActivityId: nl.ActivityNo, // 批次活动号 + CouponId: nl.CouponID, // 微信券券号 + AcquiredDate: order.ReceiveSuccessTime.Format("2006-01-02 15:04:05.000"), // 券领取时间 + Status: "1", // 券状态 0:可使用,1:已使用 + CouponStatus: "0", // 0:可使用,1:已使用 + TransDate: nl.ConsumeTime.Format("2006-01-02 15:04:05.000"), // 核销时间,验券时间,格式yyyy-mm-dd hh:mm:ss.sss + TransAmount: fmt.Sprintf("%d", nl.ConsumeAmount), + OrderId: nl.TransactionID, // 券核销支付单号 + Ticket: nl.OrderNo, // 券订单号,lsxd订单号 + OrgNo: "LANSEXIONGDIMULTI", // cmb固定值 + Attach: order.Attach, // cmb拓展参数 + Ext: "", + } + + if nl.Status.IsUsed() { + req.CouponStatus = "1" + } + + bizJsonBytes, err := json.Marshal(req) + if err != nil { + return "", fmt.Errorf("json.Marshal CmbNotifyRequest error: %v", err) + } + + return string(bizJsonBytes), nil +} + +func (biz *MultiBiz) GetRequest(ctx context.Context, nl *bo.MultiNotifyLogBo, order *bo.OrderBo) (*v1.CmbRequest, *bo.CmbRequestBo, error) { + + bizContent, err := biz.bizContent(nl, order) + if err != nil { + return nil, nil, err + } + + r := &bo.CmbRequestBo{ + FuncName: vo.CmbNotifyFuncNameUpdateCodeStatusForMulti, + BizContent: bizContent, + } + request, err := biz.CmbMixRepo.GetRequest(ctx, r) + if err != nil { + return nil, nil, err + } + + return request, r, nil +} + +func (biz *MultiBiz) Request(ctx context.Context, mmd *bo.MultiNotifyDataBo, nl *bo.MultiNotifyLogBo, request *v1.CmbRequest) error { + + if nl.RequestURL == "" { + if err := biz.notifyFail(ctx, nl, "回调通知招行地址为空,不做通知"); err != nil { + return err + } + // 回调通知地址为空,不返回错误,不做再次通知处理 + return nil + } + + reply, err := biz.CmbMixRepo.Request(ctx, request, nl.RequestURL) + if err != nil { + if err3 := biz.notifyFail(ctx, nl, err.Error()); err3 != nil { + return err3 + } + return err + } + + if err = biz.CmbMixRepo.VerifyResponse(ctx, reply); err != nil { + + errMsg := fmt.Sprintf("回调通知招行返回验证结果发生错误,resp:%+v error:%s", reply, err.Error()) + + if err3 := biz.notifyFail(ctx, nl, errMsg); err3 != nil { + return err3 + } + return err + } + + return biz.notifySuccess(ctx, mmd, nl, reply) +} + +func (biz *MultiBiz) notifyFail(ctx context.Context, nl *bo.MultiNotifyLogBo, remark string) error { + + if err := biz.MultiNotifyLogRepo.Fail(ctx, nl.ID, remark); err != nil { + return fmt.Errorf("更新通知日志失败状态发生错误 error: %v", err) + } + + return nil +} + +func (biz *MultiBiz) notifySuccess(ctx context.Context, mmd *bo.MultiNotifyDataBo, nl *bo.MultiNotifyLogBo, reply *v1.CmbReply) error { + + response, err := json.Marshal(reply) + if err != nil { + return fmt.Errorf("json.Marshal CmbReply error: %v", err) + } + + if err = biz.MultiNotifyLogRepo.Success(ctx, nl.ID, string(response)); err != nil { + return fmt.Errorf("更新通知日志成功状态发生错误 error: %v", err) + } + + if err = biz.MultiNotifyDataRepo.AddNoticeNum(ctx, mmd.ID); err != nil { + return fmt.Errorf("更新通知数据通知次数发生错误 error: %v", err) + } + + return nil +} diff --git a/internal/biz/order.go b/internal/biz/order.go index 4ef70cc..c08c612 100644 --- a/internal/biz/order.go +++ b/internal/biz/order.go @@ -3,16 +3,18 @@ package biz import ( "context" "fmt" + "time" err2 "voucher/api/err" "voucher/internal/biz/bo" + "voucher/internal/biz/businesserr" "voucher/internal/biz/vo" ) -func (this *VoucherBiz) CmbOrder(ctx context.Context, req *bo.OrderCreateReqBo) (orderNo string, err error) { +func (this *VoucherBiz) CmbOrder(ctx context.Context, req *bo.OrderCreateReqBo) (*bo.OrderBo, *bo.ProductBo, error) { order, err3 := this.GetByOutBizNo(ctx, req) if err3 != nil { - return "", err3 + return nil, nil, err3 } if order != nil { @@ -20,24 +22,39 @@ func (this *VoucherBiz) CmbOrder(ctx context.Context, req *bo.OrderCreateReqBo) if order.Status.IsFail() || order.Status.IsIng() { if err4 := this.orderRetry(ctx, order); err4 != nil { - return "", err4 + return order, nil, err4 } } - return order.OrderNo, err + return order, nil, nil } product, err3 := this.ProductRepo.GetByProductNo(ctx, req.ProductNo) if err3 != nil { - return "", err3 + return nil, product, err3 + } + + if product.StartTime == nil { + return nil, product, businesserr.BatchNotSetStartedError + } + if product.EndTime == nil { + return nil, product, businesserr.BatchSetEndedError + } + + nowTime := time.Now() + if nowTime.Before(*product.StartTime) { + return nil, product, businesserr.BatchNotStartedError + } + if nowTime.After(*product.EndTime) { + return nil, product, businesserr.BatchEndedError } order, err3 = this.order(ctx, req, product) if err3 != nil { - return "", err3 + return nil, product, err3 } - return order.OrderNo, nil + return order, product, nil } func (this *VoucherBiz) order(ctx context.Context, req *bo.OrderCreateReqBo, product *bo.ProductBo) (*bo.OrderBo, error) { @@ -105,6 +122,10 @@ func (this *VoucherBiz) create(ctx context.Context, req *bo.OrderCreateReqBo, pr ActivityId: product.ActivityId, // 多笔立减活动 } + if product.ActivityId != "" { + o.NotifyUrl = this.bc.Cmb.MultiNotifyUrl + } + return this.OrderRepo.Create(ctx, o) } @@ -126,8 +147,9 @@ func (this *VoucherBiz) fail(ctx context.Context, order *bo.OrderBo, errReq erro return err } - if errMsg == `Post "https://api.mch.weixin.qq.com/v3/marketing/favor/users/oW97fjrv_ntYBPjMsaaEMRSj6TPA/coupons": EOF` { - // 微信:不同微信号,领取了很多次,自然人限领,发放频率限制,微信研发排期,后续这个错误信息应该会更新,近期没那么快同步上去 + // 微信:不同微信号,领取了很多次,自然人限领,发放频率限制,微信研发排期,后续这个错误信息应该会更新,近期没那么快同步上去 + eqMsg := fmt.Sprintf(`Post "https://api.mch.weixin.qq.com/v3/marketing/favor/users/%s/coupons": EOF`, order.Account) + if errMsg == eqMsg { return nil } diff --git a/internal/biz/order_notify_retry.go b/internal/biz/order_notify_retry.go new file mode 100644 index 0000000..ce8b8c2 --- /dev/null +++ b/internal/biz/order_notify_retry.go @@ -0,0 +1,66 @@ +package biz + +import ( + "context" + "encoding/json" + "fmt" + "github.com/go-kratos/kratos/v2/transport/http" + "time" + "voucher/internal/biz/do" +) + +func (this *VoucherBiz) PushRetryOrderNotice(ctx http.Context, bodyBytes []byte) error { + + _, err := this.rdb.Rdb.RPush(ctx, "retryQueryNotice", string(bodyBytes)).Result() + if err != nil { + return fmt.Errorf("添加到队列失败:%v", err) + } + + return nil +} + +func (this *VoucherBiz) PushOrderNotifyRetry(ctx http.Context, req *do.OrderNotifyRetry) error { + + queue := this.bc.RdsMQ.GetOrderNotifyRetry() + if queue == nil { + return fmt.Errorf("队列不存在") + } + + msg, err := json.Marshal(req) + if err != nil { + return err + } + + strMsg := string(msg) + + _, err = this.rdb.Rdb.RPush(ctx, queue.Name, strMsg).Result() + if err != nil { + return fmt.Errorf("添加到队列失败:%v", err) + } + + return nil +} + +func (this *VoucherBiz) OrderNotifyRetry(ctx context.Context, msg string) error { + + var req *do.OrderNotifyRetry + + if err := json.Unmarshal([]byte(msg), &req); err != nil { + return err + } + + if req.StartTime == "" || req.EndTime == "" { + return fmt.Errorf("start_time or end_time is empty") + } + + start, err := time.Parse(time.DateTime, req.StartTime) + if err != nil { + return err + } + end, err := time.Parse(time.DateTime, req.EndTime) + if err != nil { + return err + } + + return this.timeSliceQuery(ctx, start, end) +} diff --git a/internal/biz/provider_set.go b/internal/biz/provider_set.go index f9f9c14..293b190 100644 --- a/internal/biz/provider_set.go +++ b/internal/biz/provider_set.go @@ -5,4 +5,4 @@ import ( ) // ProviderSetBiz is biz providers. -var ProviderSetBiz = wire.NewSet(NewVoucherBiz) +var ProviderSetBiz = wire.NewSet(NewVoucherBiz, NewMultiBiz, NewWechatBiz) diff --git a/internal/biz/query.go b/internal/biz/query.go index 30375bf..83b24c6 100644 --- a/internal/biz/query.go +++ b/internal/biz/query.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/go-kratos/kratos/v2/log" + "strings" "time" v1 "voucher/api/v1" "voucher/internal/biz/bo" @@ -66,7 +67,7 @@ func (this *VoucherBiz) Query(ctx context.Context, order *bo.OrderBo) error { return nil } -func (this *VoucherBiz) QueryOrder(ctx context.Context, orderNo string) (string, error) { +func (this *VoucherBiz) QueryOrder(ctx context.Context, orderNo, isNotice string) (string, error) { order, err3 := this.OrderRepo.GetByOrderNo(ctx, orderNo) if err3 != nil { @@ -78,5 +79,78 @@ func (this *VoucherBiz) QueryOrder(ctx context.Context, orderNo string) (string, return "", err } - return fmt.Sprintf("orderNo:%s,订单状态:%s,微信查询返回状态:%s", orderNo, order.Status.GetText(), status.GetText()), nil + notifyStr := "" + if isNotice == "YES" { + notify, err := this.Cmb.Notify(ctx, order) + if err != nil { + return "", fmt.Errorf("查询通知招行失败:%s,orderNo:%s", err, order.OrderNo) + } + notifyStr = fmt.Sprintf("通知招行成功:notify_id:%d", notify.ID) + } + + if order.Status != status { + if err = this.UpdateOrderStatus(ctx, order.ID, status); err != nil { + return "", err + } + } + + return this.ToTextDescription(order, status, notifyStr), nil +} + +func (this *VoucherBiz) ToTextDescription(bo *bo.OrderBo, orderStatus vo.OrderStatus, notifyStr string) string { + + var parts []string + + // 拼接每个字段的描述(根据业务重要性调整顺序) + parts = append(parts, fmt.Sprintf("订单ID:%d", bo.ID)) + parts = append(parts, fmt.Sprintf("订单编号:%s", bo.OrderNo)) + parts = append(parts, fmt.Sprintf("外部交易号:%s", bo.OutBizNo)) + parts = append(parts, fmt.Sprintf("券ID:%s", bo.VoucherNo)) + parts = append(parts, fmt.Sprintf("商品编号:%s", bo.ProductNo)) + parts = append(parts, fmt.Sprintf("批次号:%s", bo.BatchNo)) + parts = append(parts, fmt.Sprintf("活动ID:%s", bo.ActivityId)) + parts = append(parts, fmt.Sprintf("充值账号:%s", bo.Account)) + parts = append(parts, fmt.Sprintf("订单类型:%s", bo.Type.GetText())) // 假设 Type 有 GetText() 方法返回文字描述 + parts = append(parts, fmt.Sprintf("账号类型:%s", bo.AccountType.GetText())) + parts = append(parts, fmt.Sprintf("appid:%s", bo.AppID)) + parts = append(parts, fmt.Sprintf("制券商户:%s", bo.MerchantNo)) + parts = append(parts, fmt.Sprintf("回调地址:%s", bo.NotifyUrl)) + parts = append(parts, fmt.Sprintf("渠道:%s", bo.Channel.GetText())) + parts = append(parts, fmt.Sprintf("附加信息:%s", bo.Attach)) + parts = append(parts, fmt.Sprintf("备注:%s", bo.Remark)) + parts = append(parts, fmt.Sprintf("交易ID:%s", bo.TransactionId)) + parts = append(parts, fmt.Sprintf("订单状态:%s", bo.Status.GetText())) + parts = append(parts, fmt.Sprintf("微信查询返回状态:%s", orderStatus.GetText())) + + // 时间字段特殊处理(避免 nil 指针报错) + if bo.ReceiveSuccessTime != nil { + parts = append(parts, fmt.Sprintf("到账时间:%s", bo.ReceiveSuccessTime.Format("2006-01-02 15:04:05"))) + } else { + parts = append(parts, "到账时间:未到账") + } + + if bo.LastUseTime != nil { + parts = append(parts, fmt.Sprintf("最后使用时间:%s", bo.LastUseTime.Format("2006-01-02 15:04:05"))) + } else { + parts = append(parts, "最后使用时间:未使用") + } + + if bo.CreateTime != nil { + parts = append(parts, fmt.Sprintf("创建时间:%s", bo.CreateTime.Format("2006-01-02 15:04:05"))) + } else { + parts = append(parts, "创建时间:未知") + } + + if bo.UpdateTime != nil { + parts = append(parts, fmt.Sprintf("更新时间:%s", bo.UpdateTime.Format("2006-01-02 15:04:05"))) + } else { + parts = append(parts, "更新时间:未更新") + } + + if notifyStr != "" { + parts = append(parts, fmt.Sprintf("通知结果:%s", notifyStr)) + } + + // 用换行符拼接所有片段,形成最终描述 + return strings.Join(parts, "\n") } diff --git a/internal/biz/register_tag.go b/internal/biz/register_tag.go index aa27147..54ffb52 100644 --- a/internal/biz/register_tag.go +++ b/internal/biz/register_tag.go @@ -25,15 +25,27 @@ func (this *VoucherBiz) RegisterTag(ctx context.Context, id int32) error { return err } - if err = this.registerNotifyTag(ctx, stock.MchId, stock.BatchNo); err != nil { - return err + if this.IsNotifyRegisterTag(stock) { + if err = this.registerNotifyTag(ctx, stock.MchId, stock.BatchNo); err != nil { + return err + } } + _ = this.ProductRepo.DelCacheByProductNo(ctx, stock.ProductNo) _, err = this.ProductRepo.GetByProductNo(ctx, stock.ProductNo) return err } +func (this *VoucherBiz) IsNotifyRegisterTag(product *bo.ProductBo) bool { + for _, subject := range this.bc.WechatSubject { + if subject.MchID == product.MchId { + return false + } + } + return true +} + func (this *VoucherBiz) registerNotifyTag(ctx context.Context, stockCreatorMchID, stockID string) error { cl := vo.WechatNotifyRegisterTagCacheLockKey.BuildCache([]string{this.bc.WechatNotifyMQ.Tag, stockCreatorMchID, stockID}) diff --git a/internal/biz/repo/multi_notify_data.go b/internal/biz/repo/multi_notify_data.go new file mode 100644 index 0000000..b77adfe --- /dev/null +++ b/internal/biz/repo/multi_notify_data.go @@ -0,0 +1,14 @@ +package repo + +import ( + "context" + "voucher/internal/biz/bo" +) + +type MultiNotifyDataRepo interface { + FindNoticeNumZero(ctx context.Context, fun func(ctx context.Context, rows []*bo.MultiNotifyDataBo) error) error + Create(ctx context.Context, req *bo.MultiNotifyDataBo) (*bo.MultiNotifyDataBo, error) + GetByID(ctx context.Context, id int64) (*bo.MultiNotifyDataBo, error) + GetByNotifyID(ctx context.Context, source, notifyId string) (*bo.MultiNotifyDataBo, error) + AddNoticeNum(ctx context.Context, id int64) error +} diff --git a/internal/biz/repo/multi_notify_log.go b/internal/biz/repo/multi_notify_log.go new file mode 100644 index 0000000..470d38e --- /dev/null +++ b/internal/biz/repo/multi_notify_log.go @@ -0,0 +1,14 @@ +package repo + +import ( + "context" + "voucher/internal/biz/bo" +) + +type MultiNotifyLogRepo interface { + Create(ctx context.Context, req *bo.MultiNotifyLogBo) (*bo.MultiNotifyLogBo, error) + GetByID(ctx context.Context, id int64) (*bo.MultiNotifyLogBo, error) + ExistsSuccessByDataIDAndTransactionID(ctx context.Context, multiNotifyDataID int64, transactionID string) (bool, error) + Success(ctx context.Context, id int64, response string) error + Fail(ctx context.Context, id int64, remark string) error +} diff --git a/internal/biz/repo/order.go b/internal/biz/repo/order.go index e4be63a..f79e8b7 100644 --- a/internal/biz/repo/order.go +++ b/internal/biz/repo/order.go @@ -2,28 +2,32 @@ package repo import ( "context" + "time" "voucher/internal/biz/bo" "voucher/internal/biz/do" "voucher/internal/biz/vo" ) type OrderRepo interface { + FinUsedInBatches(ctx context.Context, req *do.WechatUsedQuery, fun func(ctx context.Context, rows []*bo.OrderBo) error) error SpecifyFindInBatches(ctx context.Context, w *bo.FindInBatchesBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error FinSucByStockIdInBatches(ctx context.Context, req *do.WechatQuery, fun func(ctx context.Context, rows []*bo.OrderBo) error) error - FinFailByStockIdInBatches(ctx context.Context, batchNo string, fun func(ctx context.Context, rows []*bo.OrderBo) error) error - FindIngInBatches(ctx context.Context, fun func(ctx context.Context, rows []*bo.OrderBo) error) error + //FinFailByStockIdInBatches(ctx context.Context, batchNo string, fun func(ctx context.Context, rows []*bo.OrderBo) error) error + //FindIngInBatches(ctx context.Context, fun func(ctx context.Context, rows []*bo.OrderBo) error) error FindInBatches(ctx context.Context, w *bo.FindInBatchesUseBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error + FindRetryQuery(ctx context.Context, req *do.RetryQueryNotice, fun func(ctx context.Context, rows []*bo.OrderBo) error) error GetByOutBizNo(ctx context.Context, t vo.OrderType, outBizNo string) (*bo.OrderBo, error) GetByOrderNo(ctx context.Context, orderNo string) (*bo.OrderBo, error) GetByCouponId(ctx context.Context, merchantNo, batchNo, voucherNo string) (*bo.OrderBo, error) - GetByTransactionId(ctx context.Context, stockCreatorMchId, stockID, transactionId string) (*bo.OrderBo, error) Create(ctx context.Context, req *bo.OrderBo) (*bo.OrderBo, error) GetByID(ctx context.Context, id uint64) (*bo.OrderBo, error) Ing(ctx context.Context, id uint64) error Success(ctx context.Context, id uint64, voucherNo string) error Fail(ctx context.Context, id uint64, remark string) error Used(ctx context.Context, id uint64) error - NotifyUsed(ctx context.Context, id uint64, transactionId string) error + NotifyUsed(ctx context.Context, id uint64, transactionId string, lastUseTime time.Time) error + MultiLastUsed(ctx context.Context, id uint64, lastUseTime time.Time) error + MultiOverUsed(ctx context.Context, id uint64, lastUseTime time.Time, remark string) error Available(ctx context.Context, id uint64) error Expired(ctx context.Context, id uint64) error } diff --git a/internal/biz/repo/orderBak.go b/internal/biz/repo/orderBak.go deleted file mode 100644 index 2c60b03..0000000 --- a/internal/biz/repo/orderBak.go +++ /dev/null @@ -1,10 +0,0 @@ -package repo - -import ( - "context" - "voucher/internal/biz/bo" -) - -type OrderBakRepo interface { - SpecifyFindInBatches(ctx context.Context, w *bo.FindInBatchesBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error -} diff --git a/internal/biz/repo/order_bak.go b/internal/biz/repo/order_bak.go new file mode 100644 index 0000000..18ed531 --- /dev/null +++ b/internal/biz/repo/order_bak.go @@ -0,0 +1,17 @@ +package repo + +import ( + "context" + "voucher/internal/biz/bo" + "voucher/internal/biz/do" +) + +type OrderBakRepo interface { + SpecifyFindInBatches(ctx context.Context, w *bo.FindInBatchesBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error + FindRetryQuery(ctx context.Context, req *do.RetryQueryNotice, fun func(ctx context.Context, rows []*bo.OrderBo) error) error + GetByID(ctx context.Context, id uint64) (*bo.OrderBo, error) + + Used(ctx context.Context, id uint64) error + Available(ctx context.Context, id uint64) error + Expired(ctx context.Context, id uint64) error +} diff --git a/internal/biz/repo/product.go b/internal/biz/repo/product.go index 1194a81..ef01d26 100644 --- a/internal/biz/repo/product.go +++ b/internal/biz/repo/product.go @@ -10,6 +10,8 @@ type ProductRepo interface { GetById(ctx context.Context, id int32) (*bo.ProductBo, error) FindWarningBudget(ctx context.Context, fun func(ctx context.Context, rows []*bo.ProductBo) error) error GetByBatchNo(ctx context.Context, batchNo string) (*bo.ProductBo, error) + GetByMchStockId(ctx context.Context, mchId, stockId string) (*bo.ProductBo, error) GetByProductNo(ctx context.Context, productNo string) (*bo.ProductBo, error) + DelCacheByProductNo(ctx context.Context, productNo string) error UpdateByWxResp(ctx context.Context, id int32, req *do.WxResp) error } diff --git a/internal/biz/timeslicequery/base.go b/internal/biz/timeslicequery/base.go index 81155bd..f3a6348 100644 --- a/internal/biz/timeslicequery/base.go +++ b/internal/biz/timeslicequery/base.go @@ -19,8 +19,9 @@ type Query struct { rdb *data.Rdb cmb *cmb.Cmb - productRepo repo.ProductRepo - orderRepo repo.OrderRepo + productRepo repo.ProductRepo + orderRepo repo.OrderRepo + orderBakRepo repo.OrderBakRepo wechatCpnRepo wechatrepo.WechatCpnRepo mqSendMixRepo mixrepos.MQSendMixRepo @@ -32,6 +33,7 @@ func NewQuery( cmb *cmb.Cmb, productRepo repo.ProductRepo, orderRepo repo.OrderRepo, + orderBakRepo repo.OrderBakRepo, wechatCpnRepo wechatrepo.WechatCpnRepo, mqSendMixRepo mixrepos.MQSendMixRepo) *Query { return &Query{ @@ -41,6 +43,7 @@ func NewQuery( cmb: cmb, productRepo: productRepo, orderRepo: orderRepo, + orderBakRepo: orderBakRepo, wechatCpnRepo: wechatCpnRepo, mqSendMixRepo: mqSendMixRepo} } diff --git a/internal/biz/timeslicequery/query.go b/internal/biz/timeslicequery/query.go index 998046c..2b2cced 100644 --- a/internal/biz/timeslicequery/query.go +++ b/internal/biz/timeslicequery/query.go @@ -7,6 +7,11 @@ import ( func (v *Query) wechatQuery(ctx context.Context, order *bo.OrderBo, useNum *int) error { + if order.Status.IsExpired() { + _, err := v.cmb.Notify(ctx, order) + return err + } + status, err := v.wechatCpnRepo.Query(ctx, order) if err != nil { return err @@ -27,7 +32,8 @@ func (v *Query) wechatQuery(ctx context.Context, order *bo.OrderBo, useNum *int) func (v *Query) queryUsed(ctx context.Context, order *bo.OrderBo) error { if order.Status.IsUse() { - return v.notify(ctx, order) + _, err := v.cmb.Notify(ctx, order) + return err } if err := v.orderRepo.Used(ctx, order.ID); err != nil { @@ -40,7 +46,8 @@ func (v *Query) queryUsed(ctx context.Context, order *bo.OrderBo) error { func (v *Query) queryExpired(ctx context.Context, order *bo.OrderBo) error { if order.Status.IsExpired() { - return nil + _, err := v.cmb.Notify(ctx, order) + return err } if err := v.orderRepo.Expired(ctx, order.ID); err != nil { @@ -50,6 +57,20 @@ func (v *Query) queryExpired(ctx context.Context, order *bo.OrderBo) error { return v.notify(ctx, order) } +func (v *Query) querySuccess(ctx context.Context, order *bo.OrderBo) error { + + if order.Status.IsSuccess() { + _, err := v.cmb.Notify(ctx, order) + return err + } + + if err := v.orderRepo.Available(ctx, order.ID); err != nil { + return err + } + + return v.notify(ctx, order) +} + func (v *Query) notify(ctx context.Context, order *bo.OrderBo) error { order, err := v.orderRepo.GetByID(ctx, order.ID) diff --git a/internal/biz/timeslicequery/query_bak.go b/internal/biz/timeslicequery/query_bak.go new file mode 100644 index 0000000..b38f75c --- /dev/null +++ b/internal/biz/timeslicequery/query_bak.go @@ -0,0 +1,62 @@ +package timeslicequery + +import ( + "context" + "voucher/internal/biz/bo" +) + +func (v *Query) queryUsedBak(ctx context.Context, order *bo.OrderBo) error { + + if order.Status.IsUse() { + _, err := v.cmb.Notify(ctx, order) + return err + } + + if err := v.orderBakRepo.Used(ctx, order.ID); err != nil { + return err + } + + return v.notify(ctx, order) +} + +func (v *Query) queryExpiredBak(ctx context.Context, order *bo.OrderBo) error { + + if order.Status.IsExpired() { + _, err := v.cmb.Notify(ctx, order) + return err + } + + if err := v.orderBakRepo.Expired(ctx, order.ID); err != nil { + return err + } + + return v.notify(ctx, order) +} + +func (v *Query) querySuccessBak(ctx context.Context, order *bo.OrderBo) error { + + if order.Status.IsSuccess() { + _, err := v.cmb.Notify(ctx, order) + return err + } + + if err := v.orderBakRepo.Available(ctx, order.ID); err != nil { + return err + } + + return v.notify(ctx, order) +} + +func (v *Query) notifyBak(ctx context.Context, order *bo.OrderBo) error { + + order, err := v.orderBakRepo.GetByID(ctx, order.ID) + if err != nil { + return err + } + + if _, err = v.cmb.Notify(ctx, order); err != nil { + return err + } + + return nil +} diff --git a/internal/biz/timeslicequery/retry_query_notice.go b/internal/biz/timeslicequery/retry_query_notice.go new file mode 100644 index 0000000..63d8ff2 --- /dev/null +++ b/internal/biz/timeslicequery/retry_query_notice.go @@ -0,0 +1,246 @@ +package timeslicequery + +import ( + "context" + "encoding/json" + "fmt" + "github.com/go-kratos/kratos/v2/log" + "github.com/hashicorp/go-multierror" + "golang.org/x/sync/errgroup" + "runtime" + "sync" + "time" + "voucher/internal/biz/bo" + "voucher/internal/biz/do" +) + +func (v *Query) RetryQueryNotice(ctx context.Context, msg string) error { + + var req *do.RetryQueryNotice + + if err := json.Unmarshal([]byte(msg), &req); err != nil { + return err + } + + err := v.RetryQueryNoticeOrder(ctx, req) + if err != nil { + return err + } + + return v.RetryQueryNoticeOrderBak(ctx, req) +} + +func (v *Query) RetryQueryNoticeOrder(ctx context.Context, req *do.RetryQueryNotice) error { + + start := time.Now() + num := 0 + errNum := 0 + sucNum := 0 + + var mu sync.Mutex + errs := make([]error, 0) + + eg := new(errgroup.Group) + eg.SetLimit(5) + + err := v.orderRepo.FindRetryQuery(ctx, req, func(ctx context.Context, rows []*bo.OrderBo) error { + + eg.Go(func() error { + + defer func() { + if err := recover(); err != nil { + // 获取调用栈信息 + _, file, line, _ := runtime.Caller(1) // 1 表示获取当前调用者的调用信息 + + mu.Lock() + errs = append(errs, fmt.Errorf("panic: %v,file:%s, line:%d", err, file, line)) + mu.Unlock() + } + }() + + for _, order := range rows { + + if err := v.retryQueryNoticeOrder(ctx, order); err != nil { + + logFields := map[string]string{ + "order_no": order.OrderNo, + "coupon_id": order.VoucherNo, + "open_id": order.Account, + "stock_id": order.BatchNo, + "err": err.Error(), + } + + log.Errorf("微信券查询order,错误:%+v", logFields) + + errNum++ + + if errNum > 20 { + return fmt.Errorf("微信券查询order,已经连续发生20次错误%+v", logFields) + } + + } else { + sucNum++ + } + } + + return nil + }) + + return nil + }) + + // 等待所有任务完成 + if err := eg.Wait(); err != nil { + return fmt.Errorf("微信券查询order,任务执行失败: %v", err) + } + + logFields := map[string]any{ + "num": num, + "sucNum": sucNum, + "errNum": errNum, + "elapsed": time.Now().Sub(start).String(), + } + log.Warnf("微信券查询order,处理完毕:%+v", logFields) + + // 收集错误 + var result error + for _, err2 := range errs { + result = multierror.Append(result, err2) + } + + return err +} + +func (v *Query) retryQueryNoticeOrder(ctx context.Context, order *bo.OrderBo) error { + + if order.Status.IsExpired() { + _, err := v.cmb.Notify(ctx, order) + return err + } + + status, err := v.wechatCpnRepo.Query(ctx, order) + if err != nil { + return err + } + + if status.IsUse() { + return v.queryUsed(ctx, order) + } else if status.IsSuccess() { + return v.querySuccess(ctx, order) + } else if status.IsExpired() { + return v.queryExpired(ctx, order) + } else { + log.Warnf("微信券查询order,未知状态orderNo:%s,status:%d", order.OrderNo, status) + } + + return nil +} + +func (v *Query) RetryQueryNoticeOrderBak(ctx context.Context, req *do.RetryQueryNotice) error { + + start := time.Now() + num := 0 + errNum := 0 + sucNum := 0 + + var mu sync.Mutex + errs := make([]error, 0) + + eg := new(errgroup.Group) + eg.SetLimit(5) + + err := v.orderBakRepo.FindRetryQuery(ctx, req, func(ctx context.Context, rows []*bo.OrderBo) error { + + eg.Go(func() error { + + defer func() { + if err := recover(); err != nil { + // 获取调用栈信息 + _, file, line, _ := runtime.Caller(1) // 1 表示获取当前调用者的调用信息 + + mu.Lock() + errs = append(errs, fmt.Errorf("panic: %v,file:%s, line:%d", err, file, line)) + mu.Unlock() + } + }() + + for _, order := range rows { + if err := v.retryQueryNoticeOrderBal(ctx, order); err != nil { + + logFields := map[string]string{ + "order_no": order.OrderNo, + "coupon_id": order.VoucherNo, + "open_id": order.Account, + "stock_id": order.BatchNo, + "err": err.Error(), + } + + log.Errorf("微信券查询orderBak,错误:%+v", logFields) + + errNum++ + + if errNum > 20 { + return fmt.Errorf("微信券查询orderBak,已经连续发生20次错误%+v", logFields) + } + + } else { + sucNum++ + } + } + + return nil + }) + + return nil + }) + + if err != nil { + return err + } + + // 等待所有任务完成 + if err2 := eg.Wait(); err2 != nil { + return fmt.Errorf("微信券查询orderBak,任务执行失败: %v", err2) + } + + logFields := map[string]any{ + "num": num, + "sucNum": sucNum, + "errNum": errNum, + "elapsed": time.Now().Sub(start).String(), + } + log.Warnf("微信券查询orderBak,处理完毕:%+v", logFields) + + // 收集错误 + var result error + for _, err2 := range errs { + result = multierror.Append(result, err2) + } + + return result +} + +func (v *Query) retryQueryNoticeOrderBal(ctx context.Context, order *bo.OrderBo) error { + + if order.Status.IsExpired() { + _, err := v.cmb.Notify(ctx, order) + return err + } + + status, err := v.wechatCpnRepo.Query(ctx, order) + if err != nil { + return err + } + + if status.IsUse() { + return v.queryUsedBak(ctx, order) + } else if status.IsSuccess() { + return v.querySuccessBak(ctx, order) + } else if status.IsExpired() { + return v.queryExpiredBak(ctx, order) + } else { + log.Warnf("微信券查询orderBak,未知状态orderNo:%s,status:%d", order.OrderNo, status) + } + + return nil +} diff --git a/internal/biz/used_notify.go b/internal/biz/used_notify.go new file mode 100644 index 0000000..8c79485 --- /dev/null +++ b/internal/biz/used_notify.go @@ -0,0 +1,104 @@ +package biz + +import ( + "context" + "encoding/json" + "fmt" + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/transport/http" + "golang.org/x/sync/errgroup" + "runtime" + "voucher/internal/biz/bo" + "voucher/internal/biz/do" +) + +func (this *VoucherBiz) UsedNotifyPush(ctx http.Context, req *do.WechatUsedQuery) error { + + queue := this.bc.RdsMQ.GetUsedNotify() + if queue == nil { + return fmt.Errorf("队列不存在") + } + + msg, err := json.Marshal(req) + if err != nil { + return err + } + + strMsg := string(msg) + + _, err = this.rdb.Rdb.RPush(ctx, queue.Name, strMsg).Result() + if err != nil { + return fmt.Errorf("添加到队列失败:%v", err) + } + + return nil +} + +func (this *VoucherBiz) UsedNotify(ctx context.Context, msg string) error { + + log.Warnf("核销重试通知处理,开始:%s", msg) + + var req *do.WechatUsedQuery + + if err := json.Unmarshal([]byte(msg), &req); err != nil { + return err + } + + errNum := 0 + + eg := new(errgroup.Group) + eg.SetLimit(3) + + err := this.OrderRepo.FinUsedInBatches(ctx, req, func(ctx context.Context, rows []*bo.OrderBo) error { + + for _, order := range rows { + + eg.Go(func() error { + + if err := this.usedNotify(ctx, order); err != nil { + errNum++ + if errNum > 50 { + return fmt.Errorf("核销重试通知处理,通知失败次数超过50次,请检查:%v", err) + } + log.Warnf("核销重试通知处理,通知失败:%v", err) + } + + return nil + }) + + } + + return nil + }) + + if err != nil { + return err + } + + return eg.Wait() // 仅返回第一个错误 +} + +func (this *VoucherBiz) usedNotify(ctx context.Context, order *bo.OrderBo) error { + + defer func() { + if err := recover(); err != nil { + _, file, line, _ := runtime.Caller(1) // 1 表示获取当前调用者的调用信息 + log.Errorf("核销重试通知处理,发生错误:req:%s,err:%v,file:%s,line:%d", order.OrderNo, err, file, line) + } + }() + + event, err := order.Status.GetOrderNotifyEvent() + if err != nil { + return err + } + + notify := &bo.OrderNotifyBo{ + OrderNo: order.OrderNo, + NotifyUrl: order.NotifyUrl, + Channel: order.Channel, + Event: event, + Type: order.Type, + } + + return this.request(ctx, order, notify) +} diff --git a/internal/biz/vo/cache.go b/internal/biz/vo/cache.go index 0650a04..23f5ff7 100644 --- a/internal/biz/vo/cache.go +++ b/internal/biz/vo/cache.go @@ -29,6 +29,10 @@ const ( ProductQueryLockKey CacheKey = "product_query_lock" ) +const ( + MultiNotifyLockKey CacheKey = "multi_notify_lock_key" +) + var ( WarningBudgetCron CacheKey = "warning_budget_cron" WarningBudgetSendIncr CacheKey = "warning_budget_incr" @@ -51,6 +55,8 @@ var CacheKeyMap = map[CacheKey]time.Duration{ WarningBudgetSendIncr: 3 * time.Hour, WarningBudgetCron: 5 * time.Minute, + + MultiNotifyLockKey: 30 * time.Second, } type Cache struct { diff --git a/internal/biz/vo/cmb.go b/internal/biz/vo/cmb.go index d89d0b1..3b22bbf 100644 --- a/internal/biz/vo/cmb.go +++ b/internal/biz/vo/cmb.go @@ -5,7 +5,8 @@ type CmbFuncName string const ( // CmbNotifyFuncName . 券状态回调通知方法 - CmbNotifyFuncName CmbFuncName = "updateCodeStatus.json" + CmbNotifyFuncName CmbFuncName = "updateCodeStatus.json" + CmbNotifyFuncNameUpdateCodeStatusForMulti CmbFuncName = "updateCodeStatusForMulti.json" ) func (s CmbFuncName) GetValue() string { diff --git a/internal/biz/vo/multi_notify_log_status.go b/internal/biz/vo/multi_notify_log_status.go new file mode 100644 index 0000000..362998d --- /dev/null +++ b/internal/biz/vo/multi_notify_log_status.go @@ -0,0 +1,38 @@ +package vo + +type MultiNotifyLogStatus int32 + +const ( + MultiNotifyLogStatusWait MultiNotifyLogStatus = iota + 1 + MultiNotifyLogStatusSuccess + MultiNotifyLogStatusFail +) + +var MultiNotifyLogStatusMap = map[MultiNotifyLogStatus]string{ + MultiNotifyLogStatusWait: "待请求", + MultiNotifyLogStatusSuccess: "请求成功", + MultiNotifyLogStatusFail: "请求失败", +} + +func (s MultiNotifyLogStatus) GetText() string { + if t, ok := MultiNotifyLogStatusMap[s]; ok { + return t + } + return "未知请求状态" +} + +func (s MultiNotifyLogStatus) GetValue() int32 { + return int32(s) +} + +func (s MultiNotifyLogStatus) IsWait() bool { + return s == MultiNotifyLogStatusWait +} + +func (s MultiNotifyLogStatus) IsSuccess() bool { + return s == MultiNotifyLogStatusSuccess +} + +func (s MultiNotifyLogStatus) IsFail() bool { + return s == MultiNotifyLogStatusFail +} diff --git a/internal/biz/voucher.go b/internal/biz/voucher.go index e21e639..9e743be 100644 --- a/internal/biz/voucher.go +++ b/internal/biz/voucher.go @@ -26,6 +26,7 @@ type VoucherBiz struct { DingMixRepo mixrepos.DingMixRepo CmbMixRepo mixrepos.CmbMixRepo SmsMixRepo mixrepos.SmsMixRepo + MultiBiz *MultiBiz mu sync.RWMutex queryMap map[string]bool @@ -47,6 +48,7 @@ func NewVoucherBiz( DingMixRepo mixrepos.DingMixRepo, CmbMixRepo mixrepos.CmbMixRepo, SmsMixRepo mixrepos.SmsMixRepo, + MultiBiz *MultiBiz, ) *VoucherBiz { return &VoucherBiz{ bc: bc, @@ -64,6 +66,7 @@ func NewVoucherBiz( DingMixRepo: DingMixRepo, CmbMixRepo: CmbMixRepo, SmsMixRepo: SmsMixRepo, + MultiBiz: MultiBiz, queryMap: make(map[string]bool), } diff --git a/internal/biz/wechat.go b/internal/biz/wechat.go new file mode 100644 index 0000000..f3db1ad --- /dev/null +++ b/internal/biz/wechat.go @@ -0,0 +1,36 @@ +package biz + +import ( + "context" + "net/http" + "voucher/internal/biz/bo" + "voucher/internal/biz/wechatrepo" +) + +type WechatBiz struct { + BankMultiActivityRepo wechatrepo.BankMultiActivityRepo +} + +func NewWechatBiz(bankMultiActivityRepo wechatrepo.BankMultiActivityRepo) *WechatBiz { + return &WechatBiz{BankMultiActivityRepo: bankMultiActivityRepo} +} + +func (biz *WechatBiz) CallBack(ctx context.Context, mchId string, headers *http.Header, respBody []byte) (*bo.WechatVoucherNotifyBo, error) { + + response, err := biz.BankMultiActivityRepo.Notify(ctx, mchId, headers, respBody) + if err != nil { + return nil, err + } + + return response, nil +} + +func (biz *WechatBiz) DecodeBody(ctx context.Context, mchId string, respBody []byte) (*bo.WechatVoucherNotifyBo, error) { + + response, err := biz.BankMultiActivityRepo.DecodeBody(ctx, mchId, respBody) + if err != nil { + return nil, err + } + + return response, nil +} diff --git a/internal/biz/wechat_notify.go b/internal/biz/wechat_notify.go index 3aa7e79..9de75dc 100644 --- a/internal/biz/wechat_notify.go +++ b/internal/biz/wechat_notify.go @@ -2,18 +2,29 @@ package biz import ( "context" - "errors" "fmt" - "gorm.io/gorm" errPb "voucher/api/err" "voucher/internal/biz/bo" "voucher/internal/biz/vo" "voucher/internal/pkg/lock" ) -func (this *VoucherBiz) WechatNotifyConsumer(ctx context.Context, tag string, req *bo.WechatVoucherNotifyBo) error { +func (this *VoucherBiz) WechatNotifyConsumer(ctx context.Context, ip string, req *bo.WechatVoucherNotifyBo) error { - c := vo.WechatNotifyConsumeLockKey.BuildCache([]string{tag, req.PlainText.StockID, req.PlainText.CouponID}) + if req.PlainText.StockCreatorMchid == "" || req.PlainText.StockID == "" || req.PlainText.CouponID == "" { + return fmt.Errorf("回调必要信息不能为空") + } + + // 商品数据量较少,先查询商品是否存在,过滤多余的通知信息 + _, err := this.ProductRepo.GetByMchStockId(ctx, req.PlainText.StockCreatorMchid, req.PlainText.StockID) + if err != nil { + //if errors.Is(err, gorm.ErrRecordNotFound) { + // return nil + //} + return fmt.Errorf("商品查询错误 error: %w", err) + } + + c := vo.WechatNotifyConsumeLockKey.BuildCache([]string{req.PlainText.StockCreatorMchid, req.PlainText.StockID, req.PlainText.CouponID}) return lock.NewMutex(this.rdb.Rdb, c.TTL).Lock(ctx, c.Key, func(ctx context.Context) error { @@ -22,6 +33,13 @@ func (this *VoucherBiz) WechatNotifyConsumer(ctx context.Context, tag string, re return err } + if order.ActivityId != "" { + if err = req.ValidateMultiNotify(); err != nil { + return fmt.Errorf("multi validate req error: %v", err) + } + return this.MultiBiz.Run(ctx, ip, req.PlainText.StockCreatorMchid, req, order) + } + if req.PlainText.Status.IsSended() { return this.available(ctx, order) @@ -33,7 +51,6 @@ func (this *VoucherBiz) WechatNotifyConsumer(ctx context.Context, tag string, re } else if req.PlainText.Status.IsExpired() { return this.expired(ctx, order) - } return fmt.Errorf("未知通知类型:%s", req.PlainText.Status.GetText()) @@ -43,31 +60,8 @@ func (this *VoucherBiz) WechatNotifyConsumer(ctx context.Context, tag string, re func (this *VoucherBiz) getOrder(ctx context.Context, req *bo.WechatVoucherNotifyBo) (*bo.OrderBo, error) { order, err := this.OrderRepo.GetByCouponId(ctx, req.PlainText.StockCreatorMchid, req.PlainText.StockID, req.PlainText.CouponID) - if err != nil { - - if !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - - order, err = this.OrderRepo.GetByTransactionId(ctx, req.PlainText.StockCreatorMchid, req.PlainText.StockID, req.PlainText.ConsumeInformation.TransactionID) - - if err != nil { - - if errors.Is(err, gorm.ErrRecordNotFound) { - - return nil, fmt.Errorf("微信回调消费,订单不存在,StockCreatorMchid:%s,StockID:%s,CouponID:%s,CreateTime:%s", - req.PlainText.StockCreatorMchid, - req.PlainText.StockID, - req.PlainText.CouponID, - req.PlainText.CreateTime, - ) - } - - return nil, err - } - - return order, nil + return nil, fmt.Errorf("订单查询错误 error: %w", err) } return order, nil @@ -80,10 +74,17 @@ func (this *VoucherBiz) notifyUsed(ctx context.Context, order *bo.OrderBo, req * return nil } - if err := this.OrderRepo.NotifyUsed(ctx, order.ID, req.PlainText.ConsumeInformation.TransactionID); err != nil { + err := this.OrderRepo.NotifyUsed( + ctx, + order.ID, + req.PlainText.ConsumeInformation.TransactionID, + req.PlainText.ConsumeInformation.ConsumeTime, + ) + if err != nil { return err } + order.LastUseTime = &req.PlainText.ConsumeInformation.ConsumeTime return this.notify(ctx, order) } @@ -117,20 +118,21 @@ func (this *VoucherBiz) expired(ctx context.Context, order *bo.OrderBo) error { func (this *VoucherBiz) notify(ctx context.Context, order *bo.OrderBo) error { - if order.ActivityId == "" { + if order.ActivityId != "" { return nil // 多笔立减活动,不做通知(?不知道有没有核销通知,多笔核销情况未知) } - return this.cmbNotify(ctx, order.ID) + return this.cmbNotify(ctx, order) } -func (this *VoucherBiz) cmbNotify(ctx context.Context, orderId uint64) error { +func (this *VoucherBiz) cmbNotify(ctx context.Context, orderReq *bo.OrderBo) error { - order, err := this.OrderRepo.GetByID(ctx, orderId) + order, err := this.OrderRepo.GetByID(ctx, orderReq.ID) if err != nil { return err } + order.LastUseTime = orderReq.LastUseTime if orderNotify, err2 := this.Cmb.Notify(ctx, order); err != nil { if !errPb.IsNeedRetryNotify(err2) { diff --git a/internal/biz/wechatrepo/bank_multi_activity.go b/internal/biz/wechatrepo/bank_multi_activity.go index 640efbf..7dcf05d 100644 --- a/internal/biz/wechatrepo/bank_multi_activity.go +++ b/internal/biz/wechatrepo/bank_multi_activity.go @@ -1,9 +1,13 @@ package wechatrepo import ( + "context" + "net/http" "voucher/internal/biz/bo" ) type BankMultiActivityRepo interface { Order(order *bo.OrderBo) (couponId string, err error) + Notify(ctx context.Context, mchId string, headers *http.Header, respBody []byte) (response *bo.WechatVoucherNotifyBo, err error) + DecodeBody(ctx context.Context, mchId string, respBody []byte) (*bo.WechatVoucherNotifyBo, error) } diff --git a/internal/conf/conf.pb.go b/internal/conf/conf.pb.go index da881e7..298901e 100644 --- a/internal/conf/conf.pb.go +++ b/internal/conf/conf.pb.go @@ -26,17 +26,19 @@ type Bootstrap struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Server *Server `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` - Logs *Logs `protobuf:"bytes,2,opt,name=logs,proto3" json:"logs,omitempty"` - Data *Data `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` - RocketMQ *RocketMQ `protobuf:"bytes,4,opt,name=rocketMQ,proto3" json:"rocketMQ,omitempty"` - Wechat *Wechat `protobuf:"bytes,5,opt,name=wechat,proto3" json:"wechat,omitempty"` - Cmb *Cmb `protobuf:"bytes,6,opt,name=cmb,proto3" json:"cmb,omitempty"` - WechatNotifyMQ *WechatNotifyMQ `protobuf:"bytes,7,opt,name=wechatNotifyMQ,proto3" json:"wechatNotifyMQ,omitempty"` - Alarm *Alarm `protobuf:"bytes,8,opt,name=alarm,proto3" json:"alarm,omitempty"` - Cron *Cron `protobuf:"bytes,9,opt,name=cron,proto3" json:"cron,omitempty"` - RdsMQ *RdsMQ `protobuf:"bytes,10,opt,name=rdsMQ,proto3" json:"rdsMQ,omitempty"` - AliYunSms *AliYunSms `protobuf:"bytes,11,opt,name=aliYunSms,proto3" json:"aliYunSms,omitempty"` + Server *Server `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + Logs *Logs `protobuf:"bytes,2,opt,name=logs,proto3" json:"logs,omitempty"` + Data *Data `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` + RocketMQ *RocketMQ `protobuf:"bytes,4,opt,name=rocketMQ,proto3" json:"rocketMQ,omitempty"` + Wechat *Wechat `protobuf:"bytes,5,opt,name=wechat,proto3" json:"wechat,omitempty"` + Cmb *Cmb `protobuf:"bytes,6,opt,name=cmb,proto3" json:"cmb,omitempty"` + WechatNotifyMQ *WechatNotifyMQ `protobuf:"bytes,7,opt,name=wechatNotifyMQ,proto3" json:"wechatNotifyMQ,omitempty"` + Alarm *Alarm `protobuf:"bytes,8,opt,name=alarm,proto3" json:"alarm,omitempty"` + Cron *Cron `protobuf:"bytes,9,opt,name=cron,proto3" json:"cron,omitempty"` + RdsMQ *RdsMQ `protobuf:"bytes,10,opt,name=rdsMQ,proto3" json:"rdsMQ,omitempty"` + AliYunSms *AliYunSms `protobuf:"bytes,11,opt,name=aliYunSms,proto3" json:"aliYunSms,omitempty"` + Tripartite *Tripartite `protobuf:"bytes,12,opt,name=tripartite,proto3" json:"tripartite,omitempty"` + WechatSubject []*WechatSubject `protobuf:"bytes,13,rep,name=wechatSubject,proto3" json:"wechatSubject,omitempty"` } func (x *Bootstrap) Reset() { @@ -148,6 +150,20 @@ func (x *Bootstrap) GetAliYunSms() *AliYunSms { return nil } +func (x *Bootstrap) GetTripartite() *Tripartite { + if x != nil { + return x.Tripartite + } + return nil +} + +func (x *Bootstrap) GetWechatSubject() []*WechatSubject { + if x != nil { + return x.WechatSubject + } + return nil +} + type Server struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -486,6 +502,7 @@ type Cmb struct { CmbKeyAlias string `protobuf:"bytes,8,opt,name=cmbKeyAlias,proto3" json:"cmbKeyAlias,omitempty"` OrgNo string `protobuf:"bytes,9,opt,name=orgNo,proto3" json:"orgNo,omitempty"` NotifyUrl string `protobuf:"bytes,10,opt,name=notifyUrl,proto3" json:"notifyUrl,omitempty"` + MultiNotifyUrl string `protobuf:"bytes,13,opt,name=multiNotifyUrl,proto3" json:"multiNotifyUrl,omitempty"` NoticeStartDays int64 `protobuf:"varint,11,opt,name=noticeStartDays,proto3" json:"noticeStartDays,omitempty"` NoticeEndDays int64 `protobuf:"varint,12,opt,name=noticeEndDays,proto3" json:"noticeEndDays,omitempty"` } @@ -592,6 +609,13 @@ func (x *Cmb) GetNotifyUrl() string { return "" } +func (x *Cmb) GetMultiNotifyUrl() string { + if x != nil { + return x.MultiNotifyUrl + } + return "" +} + func (x *Cmb) GetNoticeStartDays() int64 { if x != nil { return x.NoticeStartDays @@ -868,6 +892,8 @@ type RdsMQ struct { WechatTimeSliceQuery *RdsMQ_Queue `protobuf:"bytes,2,opt,name=wechatTimeSliceQuery,proto3" json:"wechatTimeSliceQuery,omitempty"` WechatRetry *RdsMQ_Queue `protobuf:"bytes,3,opt,name=wechatRetry,proto3" json:"wechatRetry,omitempty"` RetryNotify *RdsMQ_Queue `protobuf:"bytes,4,opt,name=retryNotify,proto3" json:"retryNotify,omitempty"` + OrderNotifyRetry *RdsMQ_Queue `protobuf:"bytes,5,opt,name=orderNotifyRetry,proto3" json:"orderNotifyRetry,omitempty"` + UsedNotify *RdsMQ_Queue `protobuf:"bytes,6,opt,name=usedNotify,proto3" json:"usedNotify,omitempty"` } func (x *RdsMQ) Reset() { @@ -930,6 +956,20 @@ func (x *RdsMQ) GetRetryNotify() *RdsMQ_Queue { return nil } +func (x *RdsMQ) GetOrderNotifyRetry() *RdsMQ_Queue { + if x != nil { + return x.OrderNotifyRetry + } + return nil +} + +func (x *RdsMQ) GetUsedNotify() *RdsMQ_Queue { + if x != nil { + return x.UsedNotify + } + return nil +} + type AliYunSms struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1009,6 +1049,53 @@ func (x *AliYunSms) GetTemplateWarning() string { return "" } +type Tripartite struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + QiXing *Tripartite_QiXing `protobuf:"bytes,1,opt,name=qiXing,proto3" json:"qiXing,omitempty"` +} + +func (x *Tripartite) Reset() { + *x = Tripartite{} + if protoimpl.UnsafeEnabled { + mi := &file_conf_conf_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Tripartite) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Tripartite) ProtoMessage() {} + +func (x *Tripartite) ProtoReflect() protoreflect.Message { + mi := &file_conf_conf_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Tripartite.ProtoReflect.Descriptor instead. +func (*Tripartite) Descriptor() ([]byte, []int) { + return file_conf_conf_proto_rawDescGZIP(), []int{12} +} + +func (x *Tripartite) GetQiXing() *Tripartite_QiXing { + if x != nil { + return x.QiXing + } + return nil +} + type Logs struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1021,7 +1108,7 @@ type Logs struct { func (x *Logs) Reset() { *x = Logs{} if protoimpl.UnsafeEnabled { - mi := &file_conf_conf_proto_msgTypes[12] + mi := &file_conf_conf_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1034,7 +1121,7 @@ func (x *Logs) String() string { func (*Logs) ProtoMessage() {} func (x *Logs) ProtoReflect() protoreflect.Message { - mi := &file_conf_conf_proto_msgTypes[12] + mi := &file_conf_conf_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1047,7 +1134,7 @@ func (x *Logs) ProtoReflect() protoreflect.Message { // Deprecated: Use Logs.ProtoReflect.Descriptor instead. func (*Logs) Descriptor() ([]byte, []int) { - return file_conf_conf_proto_rawDescGZIP(), []int{12} + return file_conf_conf_proto_rawDescGZIP(), []int{13} } func (x *Logs) GetBusiness() string { @@ -1064,6 +1151,85 @@ func (x *Logs) GetAccess() string { return "" } +type WechatSubject struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MchID string `protobuf:"bytes,1,opt,name=mchID,proto3" json:"mchID,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,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"` + MchApiV3Key string `protobuf:"bytes,6,opt,name=mchApiV3Key,proto3" json:"mchApiV3Key,omitempty"` +} + +func (x *WechatSubject) Reset() { + *x = WechatSubject{} + if protoimpl.UnsafeEnabled { + mi := &file_conf_conf_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WechatSubject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WechatSubject) ProtoMessage() {} + +func (x *WechatSubject) ProtoReflect() protoreflect.Message { + mi := &file_conf_conf_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WechatSubject.ProtoReflect.Descriptor instead. +func (*WechatSubject) Descriptor() ([]byte, []int) { + return file_conf_conf_proto_rawDescGZIP(), []int{14} +} + +func (x *WechatSubject) GetMchID() string { + if x != nil { + return x.MchID + } + return "" +} + +func (x *WechatSubject) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *WechatSubject) GetMchCertificateSerialNumber() string { + if x != nil { + return x.MchCertificateSerialNumber + } + return "" +} + +func (x *WechatSubject) GetWechatPayPublicKeyID() string { + if x != nil { + return x.WechatPayPublicKeyID + } + return "" +} + +func (x *WechatSubject) GetMchApiV3Key() string { + if x != nil { + return x.MchApiV3Key + } + return "" +} + type Server_HTTP struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1079,7 +1245,7 @@ type Server_HTTP struct { func (x *Server_HTTP) Reset() { *x = Server_HTTP{} if protoimpl.UnsafeEnabled { - mi := &file_conf_conf_proto_msgTypes[13] + mi := &file_conf_conf_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1092,7 +1258,7 @@ func (x *Server_HTTP) String() string { func (*Server_HTTP) ProtoMessage() {} func (x *Server_HTTP) ProtoReflect() protoreflect.Message { - mi := &file_conf_conf_proto_msgTypes[13] + mi := &file_conf_conf_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1159,7 +1325,7 @@ type Data_Database struct { func (x *Data_Database) Reset() { *x = Data_Database{} if protoimpl.UnsafeEnabled { - mi := &file_conf_conf_proto_msgTypes[14] + mi := &file_conf_conf_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1172,7 +1338,7 @@ func (x *Data_Database) String() string { func (*Data_Database) ProtoMessage() {} func (x *Data_Database) ProtoReflect() protoreflect.Message { - mi := &file_conf_conf_proto_msgTypes[14] + mi := &file_conf_conf_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1249,7 +1415,7 @@ type Data_Redis struct { func (x *Data_Redis) Reset() { *x = Data_Redis{} if protoimpl.UnsafeEnabled { - mi := &file_conf_conf_proto_msgTypes[15] + mi := &file_conf_conf_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1262,7 +1428,7 @@ func (x *Data_Redis) String() string { func (*Data_Redis) ProtoMessage() {} func (x *Data_Redis) ProtoReflect() protoreflect.Message { - mi := &file_conf_conf_proto_msgTypes[15] + mi := &file_conf_conf_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1353,7 +1519,7 @@ type Cron_CommandMap struct { func (x *Cron_CommandMap) Reset() { *x = Cron_CommandMap{} if protoimpl.UnsafeEnabled { - mi := &file_conf_conf_proto_msgTypes[17] + mi := &file_conf_conf_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1366,7 +1532,7 @@ func (x *Cron_CommandMap) String() string { func (*Cron_CommandMap) ProtoMessage() {} func (x *Cron_CommandMap) ProtoReflect() protoreflect.Message { - mi := &file_conf_conf_proto_msgTypes[17] + mi := &file_conf_conf_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1411,7 +1577,7 @@ type RdsMQ_Queue struct { func (x *RdsMQ_Queue) Reset() { *x = RdsMQ_Queue{} if protoimpl.UnsafeEnabled { - mi := &file_conf_conf_proto_msgTypes[19] + mi := &file_conf_conf_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1424,7 +1590,7 @@ func (x *RdsMQ_Queue) String() string { func (*RdsMQ_Queue) ProtoMessage() {} func (x *RdsMQ_Queue) ProtoReflect() protoreflect.Message { - mi := &file_conf_conf_proto_msgTypes[19] + mi := &file_conf_conf_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1475,6 +1641,53 @@ func (x *RdsMQ_Queue) GetWaitTime() *durationpb.Duration { return nil } +type Tripartite_QiXing struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AppKey string `protobuf:"bytes,1,opt,name=appKey,proto3" json:"appKey,omitempty"` +} + +func (x *Tripartite_QiXing) Reset() { + *x = Tripartite_QiXing{} + if protoimpl.UnsafeEnabled { + mi := &file_conf_conf_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Tripartite_QiXing) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Tripartite_QiXing) ProtoMessage() {} + +func (x *Tripartite_QiXing) ProtoReflect() protoreflect.Message { + mi := &file_conf_conf_proto_msgTypes[22] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Tripartite_QiXing.ProtoReflect.Descriptor instead. +func (*Tripartite_QiXing) Descriptor() ([]byte, []int) { + return file_conf_conf_proto_rawDescGZIP(), []int{12, 0} +} + +func (x *Tripartite_QiXing) GetAppKey() string { + if x != nil { + return x.AppKey + } + return "" +} + var File_conf_conf_proto protoreflect.FileDescriptor var file_conf_conf_proto_rawDesc = []byte{ @@ -1482,7 +1695,7 @@ var file_conf_conf_proto_rawDesc = []byte{ 0x6f, 0x12, 0x0e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0xa1, 0x04, 0x0a, 0x09, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x12, + 0x6f, 0x22, 0xa2, 0x05, 0x0a, 0x09, 0x42, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, @@ -1516,215 +1729,254 @@ var file_conf_conf_proto_rawDesc = []byte{ 0x61, 0x6c, 0x69, 0x59, 0x75, 0x6e, 0x53, 0x6d, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x41, 0x6c, 0x69, 0x59, 0x75, 0x6e, 0x53, 0x6d, 0x73, 0x52, 0x09, 0x61, 0x6c, 0x69, 0x59, - 0x75, 0x6e, 0x53, 0x6d, 0x73, 0x22, 0xff, 0x01, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x12, 0x2f, 0x0a, 0x04, 0x68, 0x74, 0x74, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, - 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x52, 0x04, 0x68, 0x74, 0x74, - 0x70, 0x1a, 0xc3, 0x01, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, - 0x6f, 0x75, 0x74, 0x18, 0x03, 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, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x24, 0x0a, - 0x0d, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x77, 0x61, 0x67, - 0x67, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x14, 0x69, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x52, 0x65, 0x71, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x14, 0x69, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x65, 0x71, - 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x22, 0x94, 0x05, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, - 0x12, 0x2d, 0x0a, 0x02, 0x64, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x76, - 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x44, 0x61, - 0x74, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x02, 0x64, 0x62, 0x12, - 0x30, 0x0a, 0x05, 0x72, 0x65, 0x64, 0x69, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, - 0x44, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x64, 0x69, 0x73, 0x52, 0x05, 0x72, 0x65, 0x64, 0x69, - 0x73, 0x1a, 0xc5, 0x01, 0x0a, 0x08, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, 0x16, - 0x0a, 0x06, 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, - 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x07, 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x4f, - 0x70, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x6d, 0x61, 0x78, 0x4f, 0x70, - 0x65, 0x6e, 0x12, 0x3b, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x75, 0x6e, 0x53, 0x6d, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x74, 0x72, 0x69, 0x70, 0x61, 0x72, 0x74, + 0x69, 0x74, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x76, 0x6f, 0x75, 0x63, + 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x54, 0x72, 0x69, 0x70, 0x61, + 0x72, 0x74, 0x69, 0x74, 0x65, 0x52, 0x0a, 0x74, 0x72, 0x69, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, + 0x65, 0x12, 0x43, 0x0a, 0x0d, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, + 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x57, 0x65, 0x63, 0x68, 0x61, 0x74, + 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x0d, 0x77, 0x65, 0x63, 0x68, 0x61, 0x74, 0x53, + 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x22, 0xff, 0x01, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x12, 0x2f, 0x0a, 0x04, 0x68, 0x74, 0x74, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x48, 0x54, 0x54, 0x50, 0x52, 0x04, 0x68, 0x74, + 0x74, 0x70, 0x1a, 0xc3, 0x01, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x12, 0x18, 0x0a, 0x07, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x33, 0x0a, 0x07, 0x74, 0x69, 0x6d, + 0x65, 0x6f, 0x75, 0x74, 0x18, 0x03, 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, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x24, + 0x0a, 0x0d, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x73, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x77, 0x61, + 0x67, 0x67, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x14, 0x69, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x52, 0x65, 0x71, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x14, 0x69, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x65, + 0x71, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x22, 0x94, 0x05, 0x0a, 0x04, 0x44, 0x61, 0x74, + 0x61, 0x12, 0x2d, 0x0a, 0x02, 0x64, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, + 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x44, + 0x61, 0x74, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x52, 0x02, 0x64, 0x62, + 0x12, 0x30, 0x0a, 0x05, 0x72, 0x65, 0x64, 0x69, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x44, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x64, 0x69, 0x73, 0x52, 0x05, 0x72, 0x65, 0x64, + 0x69, 0x73, 0x1a, 0xc5, 0x01, 0x0a, 0x08, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x07, 0x6d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x78, + 0x4f, 0x70, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x6d, 0x61, 0x78, 0x4f, + 0x70, 0x65, 0x6e, 0x12, 0x3b, 0x0a, 0x0b, 0x6d, 0x61, 0x78, 0x4c, 0x69, 0x66, 0x65, 0x74, 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, 0x0b, 0x6d, 0x61, 0x78, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x69, 0x73, 0x44, 0x65, 0x62, 0x75, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x69, 0x73, 0x44, 0x65, 0x62, 0x75, 0x67, 0x1a, 0xe2, 0x02, 0x0a, 0x05, 0x52, + 0x65, 0x64, 0x69, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x12, + 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x64, + 0x64, 0x72, 0x12, 0x3b, 0x0a, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, + 0x74, 0x18, 0x03, 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, 0x0b, 0x6d, 0x61, 0x78, 0x4c, 0x69, 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, - 0x18, 0x0a, 0x07, 0x69, 0x73, 0x44, 0x65, 0x62, 0x75, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x69, 0x73, 0x44, 0x65, 0x62, 0x75, 0x67, 0x1a, 0xe2, 0x02, 0x0a, 0x05, 0x52, 0x65, - 0x64, 0x69, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x12, 0x0a, - 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x64, 0x64, - 0x72, 0x12, 0x3b, 0x0a, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, - 0x18, 0x03, 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, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x3d, - 0x0a, 0x0c, 0x77, 0x72, 0x69, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x04, - 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, - 0x0c, 0x77, 0x72, 0x69, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x1a, 0x0a, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, 0x6f, - 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x6f, 0x6f, - 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x6d, 0x69, 0x6e, 0x49, 0x64, 0x6c, 0x65, - 0x43, 0x6f, 0x6e, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6d, 0x69, 0x6e, - 0x49, 0x64, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, - 0x6e, 0x4d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x08, 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, 0x0f, 0x63, - 0x6f, 0x6e, 0x6e, 0x4d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x0e, - 0x0a, 0x02, 0x64, 0x62, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x64, 0x62, 0x22, 0x97, - 0x02, 0x0a, 0x08, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x4d, 0x51, 0x12, 0x12, 0x0a, 0x04, 0x61, - 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, - 0x1c, 0x0a, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, - 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x73, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x42, 0x0a, - 0x08, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x61, 0x70, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x26, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x2e, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x4d, 0x51, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, - 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x61, - 0x70, 0x1a, 0x55, 0x0a, 0x0d, 0x45, 0x76, 0x65, 0x6e, 0x74, 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, 0x2e, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x61, 0x70, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x88, 0x01, 0x0a, 0x08, 0x45, 0x76, 0x65, - 0x6e, 0x74, 0x4d, 0x61, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x74, - 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, - 0x63, 0x12, 0x28, 0x0a, 0x0f, 0x70, 0x65, 0x72, 0x43, 0x6f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, - 0x65, 0x43, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x70, 0x65, 0x72, 0x43, - 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, 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, - 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 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, 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, 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, + 0x6f, 0x6e, 0x52, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, + 0x3d, 0x0a, 0x0c, 0x77, 0x72, 0x69, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, + 0x04, 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, 0x0c, 0x77, 0x72, 0x69, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6f, + 0x6f, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x6f, + 0x6f, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x6d, 0x69, 0x6e, 0x49, 0x64, 0x6c, + 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6d, 0x69, + 0x6e, 0x49, 0x64, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x73, 0x12, 0x43, 0x0a, 0x0f, 0x63, 0x6f, + 0x6e, 0x6e, 0x4d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x08, 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, 0x0f, + 0x63, 0x6f, 0x6e, 0x6e, 0x4d, 0x61, 0x78, 0x49, 0x64, 0x6c, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x64, 0x62, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x64, 0x62, 0x22, + 0x97, 0x02, 0x0a, 0x08, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x4d, 0x51, 0x12, 0x12, 0x0a, 0x04, + 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, + 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x1c, + 0x0a, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x20, 0x0a, 0x0b, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x42, + 0x0a, 0x08, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x61, 0x70, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x26, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x2e, 0x52, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x4d, 0x51, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4d, + 0x61, 0x70, 0x1a, 0x55, 0x0a, 0x0d, 0x45, 0x76, 0x65, 0x6e, 0x74, 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, 0x2e, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x61, 0x70, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x88, 0x01, 0x0a, 0x08, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x4d, 0x61, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x14, 0x0a, 0x05, + 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, + 0x69, 0x63, 0x12, 0x28, 0x0a, 0x0f, 0x70, 0x65, 0x72, 0x43, 0x6f, 0x72, 0x6f, 0x75, 0x74, 0x69, + 0x6e, 0x65, 0x43, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x70, 0x65, 0x72, + 0x43, 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, 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, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 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, 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, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xff, 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, 0x26, 0x0a, 0x0e, 0x6d, 0x75, 0x6c, 0x74, 0x69, + 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x55, 0x72, 0x6c, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x4e, 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, 0xc4, 0x04, + 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, 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, + 0x4d, 0x51, 0x2e, 0x51, 0x75, 0x65, 0x75, 0x65, 0x52, 0x0b, 0x72, 0x65, 0x74, 0x72, 0x79, 0x4e, + 0x6f, 0x74, 0x69, 0x66, 0x79, 0x12, 0x47, 0x0a, 0x10, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x4e, 0x6f, + 0x74, 0x69, 0x66, 0x79, 0x52, 0x65, 0x74, 0x72, 0x79, 0x18, 0x05, 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, 0x10, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x52, 0x65, 0x74, 0x72, 0x79, 0x12, 0x3b, + 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x64, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x79, 0x18, 0x06, 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, + 0x0a, 0x75, 0x73, 0x65, 0x64, 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, 0x69, 0x0a, 0x0a, 0x54, 0x72, 0x69, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x65, 0x12, 0x39, + 0x0a, 0x06, 0x71, 0x69, 0x58, 0x69, 0x6e, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, + 0x2e, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, + 0x54, 0x72, 0x69, 0x70, 0x61, 0x72, 0x74, 0x69, 0x74, 0x65, 0x2e, 0x51, 0x69, 0x58, 0x69, 0x6e, + 0x67, 0x52, 0x06, 0x71, 0x69, 0x58, 0x69, 0x6e, 0x67, 0x1a, 0x20, 0x0a, 0x06, 0x51, 0x69, 0x58, + 0x69, 0x6e, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x70, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x70, 0x70, 0x4b, 0x65, 0x79, 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, 0x22, 0xcf, 0x01, 0x0a, 0x0d, 0x57, 0x65, 0x63, 0x68, + 0x61, 0x74, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x63, 0x68, + 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x63, 0x68, 0x49, 0x44, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 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, 0x65, + 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 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, 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, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x63, 0x68, 0x41, 0x70, + 0x69, 0x56, 0x33, 0x4b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x63, + 0x68, 0x41, 0x70, 0x69, 0x56, 0x33, 0x4b, 0x65, 0x79, 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 ( @@ -1739,7 +1991,7 @@ func file_conf_conf_proto_rawDescGZIP() []byte { return file_conf_conf_proto_rawDescData } -var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_conf_conf_proto_msgTypes = make([]protoimpl.MessageInfo, 23) var file_conf_conf_proto_goTypes = []any{ (*Bootstrap)(nil), // 0: voucher.config.Bootstrap (*Server)(nil), // 1: voucher.config.Server @@ -1753,19 +2005,22 @@ var file_conf_conf_proto_goTypes = []any{ (*Cron)(nil), // 9: voucher.config.Cron (*RdsMQ)(nil), // 10: voucher.config.RdsMQ (*AliYunSms)(nil), // 11: voucher.config.AliYunSms - (*Logs)(nil), // 12: voucher.config.Logs - (*Server_HTTP)(nil), // 13: voucher.config.Server.HTTP - (*Data_Database)(nil), // 14: voucher.config.Data.Database - (*Data_Redis)(nil), // 15: voucher.config.Data.Redis - nil, // 16: voucher.config.RocketMQ.EventMapEntry - (*Cron_CommandMap)(nil), // 17: voucher.config.Cron.CommandMap - nil, // 18: voucher.config.Cron.CommandMapEntry - (*RdsMQ_Queue)(nil), // 19: voucher.config.RdsMQ.Queue - (*durationpb.Duration)(nil), // 20: google.protobuf.Duration + (*Tripartite)(nil), // 12: voucher.config.Tripartite + (*Logs)(nil), // 13: voucher.config.Logs + (*WechatSubject)(nil), // 14: voucher.config.WechatSubject + (*Server_HTTP)(nil), // 15: voucher.config.Server.HTTP + (*Data_Database)(nil), // 16: voucher.config.Data.Database + (*Data_Redis)(nil), // 17: voucher.config.Data.Redis + nil, // 18: voucher.config.RocketMQ.EventMapEntry + (*Cron_CommandMap)(nil), // 19: voucher.config.Cron.CommandMap + nil, // 20: voucher.config.Cron.CommandMapEntry + (*RdsMQ_Queue)(nil), // 21: voucher.config.RdsMQ.Queue + (*Tripartite_QiXing)(nil), // 22: voucher.config.Tripartite.QiXing + (*durationpb.Duration)(nil), // 23: google.protobuf.Duration } var file_conf_conf_proto_depIdxs = []int32{ 1, // 0: voucher.config.Bootstrap.server:type_name -> voucher.config.Server - 12, // 1: voucher.config.Bootstrap.logs:type_name -> voucher.config.Logs + 13, // 1: voucher.config.Bootstrap.logs:type_name -> voucher.config.Logs 2, // 2: voucher.config.Bootstrap.data:type_name -> voucher.config.Data 3, // 3: voucher.config.Bootstrap.rocketMQ:type_name -> voucher.config.RocketMQ 5, // 4: voucher.config.Bootstrap.wechat:type_name -> voucher.config.Wechat @@ -1775,28 +2030,33 @@ var file_conf_conf_proto_depIdxs = []int32{ 9, // 8: voucher.config.Bootstrap.cron:type_name -> voucher.config.Cron 10, // 9: voucher.config.Bootstrap.rdsMQ:type_name -> voucher.config.RdsMQ 11, // 10: voucher.config.Bootstrap.aliYunSms:type_name -> voucher.config.AliYunSms - 13, // 11: voucher.config.Server.http:type_name -> voucher.config.Server.HTTP - 14, // 12: voucher.config.Data.db:type_name -> voucher.config.Data.Database - 15, // 13: voucher.config.Data.redis:type_name -> voucher.config.Data.Redis - 16, // 14: voucher.config.RocketMQ.eventMap:type_name -> voucher.config.RocketMQ.EventMapEntry - 18, // 15: voucher.config.Cron.commandMap:type_name -> voucher.config.Cron.CommandMapEntry - 19, // 16: voucher.config.RdsMQ.wechatQuery:type_name -> voucher.config.RdsMQ.Queue - 19, // 17: voucher.config.RdsMQ.wechatTimeSliceQuery:type_name -> voucher.config.RdsMQ.Queue - 19, // 18: voucher.config.RdsMQ.wechatRetry:type_name -> voucher.config.RdsMQ.Queue - 19, // 19: voucher.config.RdsMQ.retryNotify:type_name -> voucher.config.RdsMQ.Queue - 20, // 20: voucher.config.Server.HTTP.timeout:type_name -> google.protobuf.Duration - 20, // 21: voucher.config.Data.Database.maxLifetime:type_name -> google.protobuf.Duration - 20, // 22: voucher.config.Data.Redis.readTimeout:type_name -> google.protobuf.Duration - 20, // 23: voucher.config.Data.Redis.writeTimeout:type_name -> google.protobuf.Duration - 20, // 24: voucher.config.Data.Redis.connMaxIdleTime:type_name -> google.protobuf.Duration - 4, // 25: voucher.config.RocketMQ.EventMapEntry.value:type_name -> voucher.config.EventMap - 17, // 26: voucher.config.Cron.CommandMapEntry.value:type_name -> voucher.config.Cron.CommandMap - 20, // 27: voucher.config.RdsMQ.Queue.waitTime:type_name -> google.protobuf.Duration - 28, // [28:28] is the sub-list for method output_type - 28, // [28:28] is the sub-list for method input_type - 28, // [28:28] is the sub-list for extension type_name - 28, // [28:28] is the sub-list for extension extendee - 0, // [0:28] is the sub-list for field type_name + 12, // 11: voucher.config.Bootstrap.tripartite:type_name -> voucher.config.Tripartite + 14, // 12: voucher.config.Bootstrap.wechatSubject:type_name -> voucher.config.WechatSubject + 15, // 13: voucher.config.Server.http:type_name -> voucher.config.Server.HTTP + 16, // 14: voucher.config.Data.db:type_name -> voucher.config.Data.Database + 17, // 15: voucher.config.Data.redis:type_name -> voucher.config.Data.Redis + 18, // 16: voucher.config.RocketMQ.eventMap:type_name -> voucher.config.RocketMQ.EventMapEntry + 20, // 17: voucher.config.Cron.commandMap:type_name -> voucher.config.Cron.CommandMapEntry + 21, // 18: voucher.config.RdsMQ.wechatQuery:type_name -> voucher.config.RdsMQ.Queue + 21, // 19: voucher.config.RdsMQ.wechatTimeSliceQuery:type_name -> voucher.config.RdsMQ.Queue + 21, // 20: voucher.config.RdsMQ.wechatRetry:type_name -> voucher.config.RdsMQ.Queue + 21, // 21: voucher.config.RdsMQ.retryNotify:type_name -> voucher.config.RdsMQ.Queue + 21, // 22: voucher.config.RdsMQ.orderNotifyRetry:type_name -> voucher.config.RdsMQ.Queue + 21, // 23: voucher.config.RdsMQ.usedNotify:type_name -> voucher.config.RdsMQ.Queue + 22, // 24: voucher.config.Tripartite.qiXing:type_name -> voucher.config.Tripartite.QiXing + 23, // 25: voucher.config.Server.HTTP.timeout:type_name -> google.protobuf.Duration + 23, // 26: voucher.config.Data.Database.maxLifetime:type_name -> google.protobuf.Duration + 23, // 27: voucher.config.Data.Redis.readTimeout:type_name -> google.protobuf.Duration + 23, // 28: voucher.config.Data.Redis.writeTimeout:type_name -> google.protobuf.Duration + 23, // 29: voucher.config.Data.Redis.connMaxIdleTime:type_name -> google.protobuf.Duration + 4, // 30: voucher.config.RocketMQ.EventMapEntry.value:type_name -> voucher.config.EventMap + 19, // 31: voucher.config.Cron.CommandMapEntry.value:type_name -> voucher.config.Cron.CommandMap + 23, // 32: voucher.config.RdsMQ.Queue.waitTime:type_name -> google.protobuf.Duration + 33, // [33:33] is the sub-list for method output_type + 33, // [33:33] is the sub-list for method input_type + 33, // [33:33] is the sub-list for extension type_name + 33, // [33:33] is the sub-list for extension extendee + 0, // [0:33] is the sub-list for field type_name } func init() { file_conf_conf_proto_init() } @@ -1950,7 +2210,7 @@ func file_conf_conf_proto_init() { } } file_conf_conf_proto_msgTypes[12].Exporter = func(v any, i int) any { - switch v := v.(*Logs); i { + switch v := v.(*Tripartite); i { case 0: return &v.state case 1: @@ -1962,7 +2222,7 @@ func file_conf_conf_proto_init() { } } file_conf_conf_proto_msgTypes[13].Exporter = func(v any, i int) any { - switch v := v.(*Server_HTTP); i { + switch v := v.(*Logs); i { case 0: return &v.state case 1: @@ -1974,7 +2234,7 @@ func file_conf_conf_proto_init() { } } file_conf_conf_proto_msgTypes[14].Exporter = func(v any, i int) any { - switch v := v.(*Data_Database); i { + switch v := v.(*WechatSubject); i { case 0: return &v.state case 1: @@ -1986,7 +2246,19 @@ func file_conf_conf_proto_init() { } } file_conf_conf_proto_msgTypes[15].Exporter = func(v any, i int) any { - switch v := v.(*Data_Redis); i { + switch v := v.(*Server_HTTP); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_conf_conf_proto_msgTypes[16].Exporter = func(v any, i int) any { + switch v := v.(*Data_Database); i { case 0: return &v.state case 1: @@ -1998,7 +2270,7 @@ func file_conf_conf_proto_init() { } } file_conf_conf_proto_msgTypes[17].Exporter = func(v any, i int) any { - switch v := v.(*Cron_CommandMap); i { + switch v := v.(*Data_Redis); i { case 0: return &v.state case 1: @@ -2010,6 +2282,18 @@ func file_conf_conf_proto_init() { } } file_conf_conf_proto_msgTypes[19].Exporter = func(v any, i int) any { + switch v := v.(*Cron_CommandMap); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_conf_conf_proto_msgTypes[21].Exporter = func(v any, i int) any { switch v := v.(*RdsMQ_Queue); i { case 0: return &v.state @@ -2021,6 +2305,18 @@ func file_conf_conf_proto_init() { return nil } } + file_conf_conf_proto_msgTypes[22].Exporter = func(v any, i int) any { + switch v := v.(*Tripartite_QiXing); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -2028,7 +2324,7 @@ func file_conf_conf_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_conf_conf_proto_rawDesc, NumEnums: 0, - NumMessages: 20, + NumMessages: 23, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/conf/conf.proto b/internal/conf/conf.proto index 58ee42f..aabe777 100644 --- a/internal/conf/conf.proto +++ b/internal/conf/conf.proto @@ -17,6 +17,8 @@ message Bootstrap { Cron cron = 9; RdsMQ rdsMQ = 10; AliYunSms aliYunSms = 11; + Tripartite tripartite = 12; + repeated WechatSubject wechatSubject = 13; } message Server { @@ -88,6 +90,7 @@ message Cmb { string cmbKeyAlias = 8; string orgNo = 9; string notifyUrl = 10; + string multiNotifyUrl = 13; int64 noticeStartDays = 11; int64 noticeEndDays = 12; } @@ -134,6 +137,8 @@ message RdsMQ { Queue wechatTimeSliceQuery = 2; Queue wechatRetry = 3; Queue retryNotify = 4; + Queue orderNotifyRetry = 5; + Queue usedNotify = 6; } message AliYunSms { @@ -144,7 +149,22 @@ message AliYunSms { string templateWarning = 5; } +message Tripartite { + message QiXing { + string appKey = 1; + } + QiXing qiXing = 1; +} + message Logs { string business = 1; string access = 2; +} + +message WechatSubject { + string mchID = 1; + string name = 4; + string mchCertificateSerialNumber = 2; + string wechatPayPublicKeyID = 3; + string mchApiV3Key = 6; } \ No newline at end of file diff --git a/internal/data/gorm.go b/internal/data/gorm.go index 3ef0314..1459afe 100644 --- a/internal/data/gorm.go +++ b/internal/data/gorm.go @@ -36,8 +36,8 @@ func db(data *conf.Data_Database) *gorm.DB { panic("failed to gormDB " + err.Error()) } - sqlDB.SetMaxIdleConns(100) - sqlDB.SetMaxOpenConns(1000) + sqlDB.SetMaxIdleConns(50) + sqlDB.SetMaxOpenConns(200) return gormDB } diff --git a/internal/data/mixrepoimpl/cmb.go b/internal/data/mixrepoimpl/cmb.go index a35b4f2..5ed0844 100644 --- a/internal/data/mixrepoimpl/cmb.go +++ b/internal/data/mixrepoimpl/cmb.go @@ -289,19 +289,19 @@ func (s *CmbMixRepoImpl) Request(ctx context.Context, req *v1.CmbRequest, uri st _, bodyBytes, err := request.Post(ctx, r, nil, request.WithHeaders(h), request.WithTimeout(time.Second*20)) if err != nil { - log.Errorf("请求掌上生活报错,url:%s,err:%v", r, err) - return nil, err + //log.Errorf("请求掌上生活报错,url:%s,err:%v", r, err) + return nil, fmt.Errorf("CMB请求失败:%v", err) } var response *v1.CmbReply if err = json.Unmarshal(bodyBytes, &response); err != nil { log.Errorf("请求掌上生活返回数据解析报错:%s,url:%s,bodyBytes:%s", err.Error(), r, string(bodyBytes)) - return nil, err + return nil, fmt.Errorf("CMB数据解析错误:%s", err.Error()) } if response.RespCode != vo.CmbResponseStatusSuccess.GetValue() { - log.Errorf("请求掌上生活返回报错:msg:%s,url:%s,bodyBytes:%s", response.RespMsg, r, string(bodyBytes)) - return nil, fmt.Errorf(response.RespMsg) + //log.Errorf("请求掌上生活返回报错:msg:%s,url:%s,bodyBytes:%s", response.RespMsg, r, string(bodyBytes)) + return nil, fmt.Errorf("CMB请求返回错误:%s", response.RespMsg) } return response, nil diff --git a/internal/data/model/multi_notify_data.gen.go b/internal/data/model/multi_notify_data.gen.go new file mode 100644 index 0000000..85de5ea --- /dev/null +++ b/internal/data/model/multi_notify_data.gen.go @@ -0,0 +1,37 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package model + +import ( + "time" +) + +const TableNameMultiNotifyDatum = "multi_notify_data" + +// MultiNotifyDatum mapped from table +type MultiNotifyDatum struct { + ID int64 `gorm:"column:id;primaryKey" json:"id"` + IP string `gorm:"column:ip;not null;comment:ip" json:"ip"` // ip + Source string `gorm:"column:source;not null;comment:来源" json:"source"` // 来源 + NotifyID string `gorm:"column:notify_id;not null;comment:回调通知id" json:"notify_id"` // 回调通知id + OrderNo string `gorm:"column:order_no;not null;comment:订单号" json:"order_no"` // 订单号 + OutBizNo string `gorm:"column:out_biz_no;not null;comment:外部业务号" json:"out_biz_no"` // 外部业务号 + CouponID string `gorm:"column:coupon_id;not null;comment:券id" json:"coupon_id"` // 券id + StockID string `gorm:"column:stock_id;not null;comment:微信批次号" json:"stock_id"` // 微信批次号 + ConsumeAmount int32 `gorm:"column:consume_amount;not null;comment:核销金额" json:"consume_amount"` // 核销金额 + ConsumeTime *time.Time `gorm:"column:consume_time;not null;comment:核销时间" json:"consume_time"` // 核销时间 + TransactionID string `gorm:"column:transaction_id;not null;comment:微信支付系统生成的订单号" json:"transaction_id"` // 微信支付系统生成的订单号 + EventType string `gorm:"column:event_type;not null;comment:通知的类型" json:"event_type"` // 通知的类型 + Status string `gorm:"column:status;not null;comment:券状态" json:"status"` // 券状态 + OriginalData string `gorm:"column:original_data;not null;comment:微信回调通知原始数据" json:"original_data"` // 微信回调通知原始数据 + NoticeNum int32 `gorm:"column:notice_num;not null;comment:通知下游次数" json:"notice_num"` // 通知下游次数 + CreateTime *time.Time `gorm:"column:create_time;not null;comment:创建时间" json:"create_time"` // 创建时间 + UpdateTime *time.Time `gorm:"column:update_time;comment:修改时间" json:"update_time"` // 修改时间 +} + +// TableName MultiNotifyDatum's table name +func (*MultiNotifyDatum) TableName() string { + return TableNameMultiNotifyDatum +} diff --git a/internal/data/model/multi_notify_log.gen.go b/internal/data/model/multi_notify_log.gen.go new file mode 100644 index 0000000..627d601 --- /dev/null +++ b/internal/data/model/multi_notify_log.gen.go @@ -0,0 +1,41 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package model + +import ( + "time" +) + +const TableNameMultiNotifyLog = "multi_notify_log" + +// MultiNotifyLog mapped from table +type MultiNotifyLog struct { + ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"` + MultiNotifyDataID int64 `gorm:"column:multi_notify_data_id;not null" json:"multi_notify_data_id"` + OrderNo string `gorm:"column:order_no;not null;comment:订单号" json:"order_no"` // 订单号 + OutBizNo string `gorm:"column:out_biz_no;not null;comment:外部请求号" json:"out_biz_no"` // 外部请求号 + CouponID string `gorm:"column:coupon_id;not null;comment:微信券id" json:"coupon_id"` // 微信券id + ActivityNo string `gorm:"column:activity_no;not null;comment:活动编号CMB开头" json:"activity_no"` // 活动编号CMB开头 + StockID string `gorm:"column:stock_id;not null;comment:微信批次号" json:"stock_id"` // 微信批次号 + EventType string `gorm:"column:event_type;not null;comment:通知类型 COUPON.USE" json:"event_type"` // 通知类型 COUPON.USE + Status string `gorm:"column:status;not null;comment:券状态" json:"status"` // 券状态 + ConsumeAmount int32 `gorm:"column:consume_amount;not null;comment:核销金额" json:"consume_amount"` // 核销金额 + ConsumeTime *time.Time `gorm:"column:consume_time;not null;comment:核销时间" json:"consume_time"` // 核销时间 + TransactionID string `gorm:"column:transaction_id;not null;comment:微信支付系统生成的订单号" json:"transaction_id"` // 微信支付系统生成的订单号 + RequestURL string `gorm:"column:request_url;not null;comment:请求地址" json:"request_url"` // 请求地址 + RequestStatus int32 `gorm:"column:request_status;not null;comment:请求状态" json:"request_status"` // 请求状态 + OriginReq string `gorm:"column:origin_req;not null;comment:请求数据" json:"origin_req"` // 请求数据 + Request string `gorm:"column:request;not null;comment:请求数据" json:"request"` // 请求数据 + Response string `gorm:"column:response;not null;comment:响应结果" json:"response"` // 响应结果 + OrderCreateTime *time.Time `gorm:"column:order_create_time;not null;comment:券收单时间-蓝色兄弟" json:"order_create_time"` // 券收单时间-蓝色兄弟 + CouponCreateTime *time.Time `gorm:"column:coupon_create_time;not null;comment:券创建时间-微信侧" json:"coupon_create_time"` // 券创建时间-微信侧 + CreateTime *time.Time `gorm:"column:create_time;not null;comment:创建时间" json:"create_time"` // 创建时间 + UpdateTime *time.Time `gorm:"column:update_time;comment:修改时间" json:"update_time"` // 修改时间 +} + +// TableName MultiNotifyLog's table name +func (*MultiNotifyLog) TableName() string { + return TableNameMultiNotifyLog +} diff --git a/internal/data/repoimpl/multi_notify_data.go b/internal/data/repoimpl/multi_notify_data.go new file mode 100644 index 0000000..2fd3c1c --- /dev/null +++ b/internal/data/repoimpl/multi_notify_data.go @@ -0,0 +1,130 @@ +package repoimpl + +import ( + "context" + "gorm.io/gorm" + "time" + "voucher/internal/biz/bo" + "voucher/internal/biz/repo" + "voucher/internal/data" + "voucher/internal/data/model" +) + +// MultiNotifyDataRepoImpl . +type MultiNotifyDataRepoImpl struct { + Base[model.MultiNotifyDatum, bo.MultiNotifyDataBo] + db *data.Db +} + +// NewMultiNotifyDataRepoImpl . +func NewMultiNotifyDataRepoImpl(db *data.Db) repo.MultiNotifyDataRepo { + return &MultiNotifyDataRepoImpl{db: db} +} + +func (p *MultiNotifyDataRepoImpl) DB(ctx context.Context) *gorm.DB { + return p.db.DB(ctx).WithContext(ctx).Model(model.MultiNotifyDatum{}) +} + +func (p *MultiNotifyDataRepoImpl) FindNoticeNumZero(ctx context.Context, fun func(ctx context.Context, rows []*bo.MultiNotifyDataBo) error) error { + + tx := p.DB(ctx).Where("notice_num = 0") + tx.Order("id asc") // 显式清除排序,移除默认的 ORDER BY + tx.Limit(200) + + var results = make([]*model.MultiNotifyDatum, 0) + + result := tx.FindInBatches(&results, 50, func(tx *gorm.DB, batch int) error { + return fun(ctx, p.ToBos(results)) + }) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (p *MultiNotifyDataRepoImpl) Create(ctx context.Context, req *bo.MultiNotifyDataBo) (*bo.MultiNotifyDataBo, error) { + + now := time.Now() + + info := &model.MultiNotifyDatum{ + Source: req.Source, + IP: req.IP, + NotifyID: req.NotifyID, + OrderNo: req.OrderNo, + OutBizNo: req.OutBizNo, + CouponID: req.CouponID, + StockID: req.StockID, + ConsumeAmount: req.ConsumeAmount, + ConsumeTime: req.ConsumeTime, + TransactionID: req.TransactionID, + EventType: req.EventType, + Status: req.Status.GetValue(), + OriginalData: req.OriginalData, + NoticeNum: 0, + CreateTime: &now, + } + + if err := p.DB(ctx).Create(info).Error; err != nil { + return nil, err + } + + return p.ToBo(info), nil +} + +func (p *MultiNotifyDataRepoImpl) GetByID(ctx context.Context, id int64) (*bo.MultiNotifyDataBo, error) { + + var item model.MultiNotifyDatum + + tx := p.DB(ctx).Where(model.MultiNotifyDatum{ID: id}).First(&item) + + if tx.Error != nil { + return nil, tx.Error + } + + if tx.RowsAffected == 0 { + return nil, gorm.ErrRecordNotFound + } + + return p.ToBo(&item), nil +} + +func (p *MultiNotifyDataRepoImpl) GetByNotifyID(ctx context.Context, source, notifyId string) (*bo.MultiNotifyDataBo, error) { + + var item model.MultiNotifyDatum + + tx := p.DB(ctx).Where(model.MultiNotifyDatum{Source: source, NotifyID: notifyId}).First(&item) + + if tx.Error != nil { + return nil, tx.Error + } + + if tx.RowsAffected == 0 { + return nil, gorm.ErrRecordNotFound + } + + return p.ToBo(&item), nil +} + +func (p *MultiNotifyDataRepoImpl) AddNoticeNum(ctx context.Context, id int64) error { + + now := time.Now() + + u := map[string]interface{}{ + "notice_num": gorm.Expr("notice_num + ?", 1), + "update_time": &now, + } + + tx := p.DB(ctx).Where("id = ?", id).Updates(u) + + if tx.Error != nil { + return tx.Error + } + + if tx.RowsAffected == 0 { + return gorm.ErrRecordNotFound + } + + return nil +} diff --git a/internal/data/repoimpl/multi_notify_log.go b/internal/data/repoimpl/multi_notify_log.go new file mode 100644 index 0000000..ba12dca --- /dev/null +++ b/internal/data/repoimpl/multi_notify_log.go @@ -0,0 +1,149 @@ +package repoimpl + +import ( + "context" + "errors" + "fmt" + "time" + "unicode/utf8" + err2 "voucher/api/err" + "voucher/internal/biz/bo" + "voucher/internal/biz/repo" + "voucher/internal/biz/vo" + "voucher/internal/data" + "voucher/internal/data/model" + + "gorm.io/gorm" +) + +// MultiNotifyLogRepoImpl . +type MultiNotifyLogRepoImpl struct { + Base[model.MultiNotifyLog, bo.MultiNotifyLogBo] + db *data.Db +} + +// NewMultiNotifyLogRepoImpl . +func NewMultiNotifyLogRepoImpl(db *data.Db) repo.MultiNotifyLogRepo { + return &MultiNotifyLogRepoImpl{db: db} +} + +func (p *MultiNotifyLogRepoImpl) DB(ctx context.Context) *gorm.DB { + return p.db.DB(ctx).WithContext(ctx).Model(model.MultiNotifyLog{}) +} + +func (p *MultiNotifyLogRepoImpl) Create(ctx context.Context, req *bo.MultiNotifyLogBo) (*bo.MultiNotifyLogBo, error) { + + now := time.Now() + + info := &model.MultiNotifyLog{ + MultiNotifyDataID: req.MultiNotifyDataID, + OrderNo: req.OrderNo, + OutBizNo: req.OutBizNo, + CouponID: req.CouponID, + ActivityNo: req.ActivityNo, + StockID: req.StockID, + EventType: req.EventType, + Status: req.Status.GetValue(), + ConsumeAmount: req.ConsumeAmount, + ConsumeTime: req.ConsumeTime, + TransactionID: req.TransactionID, + OriginReq: req.OriginReq, + Request: req.Request, + RequestURL: req.RequestURL, + RequestStatus: vo.MultiNotifyLogStatusWait.GetValue(), + OrderCreateTime: req.OrderCreateTime, + CouponCreateTime: req.CouponCreateTime, + CreateTime: &now, + } + + if err := p.DB(ctx).Create(info).Error; err != nil { + return nil, err + } + + return p.ToBo(info), nil +} + +func (p *MultiNotifyLogRepoImpl) GetByID(ctx context.Context, id int64) (*bo.MultiNotifyLogBo, error) { + var item model.MultiNotifyLog + + tx := p.DB(ctx).Where(model.MultiNotifyLog{ID: id}).First(&item) + + if tx.Error != nil { + return nil, fmt.Errorf("b fail %w", tx.Error) + } + + if tx.RowsAffected == 0 { + return nil, err2.ErrorDbNotFound("数据不存在") + } + + return p.ToBo(&item), nil +} + +func (p *MultiNotifyLogRepoImpl) ExistsSuccessByDataIDAndTransactionID(ctx context.Context, multiNotifyDataID int64, transactionID string) (bool, error) { + var item model.MultiNotifyLog + + err := p.DB(ctx). + Select("id"). + Where("multi_notify_data_id = ? AND transaction_id = ? AND request_status = ?", multiNotifyDataID, transactionID, vo.MultiNotifyLogStatusSuccess.GetValue()). + Limit(1). + Take(&item).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + + return true, nil +} + +func (p *MultiNotifyLogRepoImpl) Success(ctx context.Context, id int64, response string) error { + + now := time.Now() + + res := p.DB(ctx). + Where(model.MultiNotifyLog{ + ID: id, + RequestStatus: vo.MultiNotifyLogStatusWait.GetValue(), + }). + Updates(model.MultiNotifyLog{ + RequestStatus: vo.MultiNotifyLogStatusSuccess.GetValue(), + Response: response, + UpdateTime: &now, + }) + + if res.Error != nil { + return res.Error + } + + return nil +} + +func (p *MultiNotifyLogRepoImpl) Fail(ctx context.Context, id int64, remark string) error { + + if utf8.RuneCountInString(remark) > 255 { + runes := []rune(remark) + if len(runes) > 255 { + remark = string(runes[:255]) + } + } + + now := time.Now() + + res := p.DB(ctx). + Where(model.MultiNotifyLog{ + ID: id, + RequestStatus: vo.MultiNotifyLogStatusWait.GetValue(), + }). + Updates(model.MultiNotifyLog{ + RequestStatus: vo.MultiNotifyLogStatusFail.GetValue(), + Response: remark, + UpdateTime: &now, + }) + + if res.Error != nil { + return res.Error + } + + return nil +} diff --git a/internal/data/repoimpl/order.go b/internal/data/repoimpl/order.go index 0e3c64a..1186fa5 100644 --- a/internal/data/repoimpl/order.go +++ b/internal/data/repoimpl/order.go @@ -55,6 +55,8 @@ func (p *OrderRepoImpl) SpecifyFindInBatches(ctx context.Context, req *bo.FindIn tx = tx.Where("voucher_no IN (?)", req.VoucherNos) } + tx.Order("receive_success_time asc") // 显式清除排序,移除默认的 ORDER BY + var results = make([]*model.Order, 0) result := tx.FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error { @@ -72,8 +74,11 @@ 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()). - Where("activity_id = ''") + Where("`status` in (?)", []uint8{ + vo.OrderStatusSuccess.GetValue(), + vo.OrderStatusUse.GetValue(), + vo.OrderStatusExpired.GetValue(), + }).Where("activity_id = ''") if req.ProductNo != "" { tx = tx.Where("product_no = ?", req.ProductNo) @@ -93,7 +98,9 @@ func (p *OrderRepoImpl) FinSucByStockIdInBatches(ctx context.Context, req *do.We var results = make([]*model.Order, 0) - result := tx.FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error { + tx.Order("receive_success_time asc") // 显式清除排序,移除默认的 ORDER BY + + result := tx.FindInBatches(&results, 1000, func(tx *gorm.DB, batch int) error { return fun(ctx, p.ToBos(results)) }) @@ -105,16 +112,34 @@ func (p *OrderRepoImpl) FinSucByStockIdInBatches(ctx context.Context, req *do.We return nil } -func (p *OrderRepoImpl) FinFailByStockIdInBatches(ctx context.Context, batchNo string, fun func(ctx context.Context, rows []*bo.OrderBo) error) error { +func (p *OrderRepoImpl) FinUsedInBatches(ctx context.Context, req *do.WechatUsedQuery, fun func(ctx context.Context, rows []*bo.OrderBo) error) error { var results = make([]*model.Order, 0) - result := p.DB(ctx). - Where("batch_no = ?", batchNo). - Where("`status` = ?", vo.OrderStatusFail.GetValue()). - FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error { - return fun(ctx, p.ToBos(results)) - }) + tx := p.DB(ctx). + Where("`status` = ?", vo.OrderStatusUse.GetValue()). + Where("activity_id = ''") + + if req.StartTime != "" { + tx = tx.Where("last_use_time > ?", req.StartTime) + } + if req.EndTime != "" { + tx = tx.Where("last_use_time <= ?", req.EndTime) + } + if req.ProductNo != "" { + tx = tx.Where("product_no = ?", req.ProductNo) + } + if req.BatchNo != "" { + tx = tx.Where("batch_no = ?", req.ProductNo) + } + if req.OrderNo != "" { + tx = tx.Where("order_no = ?", req.OrderNo) + } + + // 显式清除排序,移除默认的 ORDER BY + result := tx.Order("last_use_time asc").FindInBatches(&results, 500, func(tx *gorm.DB, batch int) error { + return fun(ctx, p.ToBos(results)) + }) if result.Error != nil { return result.Error @@ -123,23 +148,43 @@ func (p *OrderRepoImpl) FinFailByStockIdInBatches(ctx context.Context, batchNo s return nil } -func (p *OrderRepoImpl) FindIngInBatches(ctx context.Context, fun func(ctx context.Context, rows []*bo.OrderBo) error) error { +//func (p *OrderRepoImpl) FinFailByStockIdInBatches(ctx context.Context, batchNo string, fun func(ctx context.Context, rows []*bo.OrderBo) error) error { +// +// var results = make([]*model.Order, 0) +// +// result := p.DB(ctx). +// Where("batch_no = ?", batchNo). +// Where("`status` = ?", vo.OrderStatusFail.GetValue()). +// Order("receive_success_time asc"). // 显式清除排序,移除默认的 ORDER BY +// FindInBatches(&results, 200, func(tx *gorm.DB, batch int) error { +// return fun(ctx, p.ToBos(results)) +// }) +// +// if result.Error != nil { +// return result.Error +// } +// +// return nil +//} - var results = make([]*model.Order, 0) - - result := p.DB(ctx). - Where("`status` = ?", vo.OrderStatusIng.GetValue()). - Limit(20). - FindInBatches(&results, 10, func(tx *gorm.DB, batch int) error { - return fun(ctx, p.ToBos(results)) - }) - - if result.Error != nil { - return result.Error - } - - return nil -} +//func (p *OrderRepoImpl) FindIngInBatches(ctx context.Context, fun func(ctx context.Context, rows []*bo.OrderBo) error) error { +// +// var results = make([]*model.Order, 0) +// +// result := p.DB(ctx). +// Where("`status` = ?", vo.OrderStatusIng.GetValue()). +// Limit(20). +// Order("receive_success_time asc"). // 显式清除排序,移除默认的 ORDER BY +// FindInBatches(&results, 10, func(tx *gorm.DB, batch int) error { +// return fun(ctx, p.ToBos(results)) +// }) +// +// if result.Error != nil { +// return result.Error +// } +// +// return nil +//} func (p *OrderRepoImpl) FindInBatches(ctx context.Context, req *bo.FindInBatchesUseBo, fun func(ctx context.Context, rows []*bo.OrderBo) error) error { @@ -149,7 +194,8 @@ func (p *OrderRepoImpl) FindInBatches(ctx context.Context, req *bo.FindInBatches 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 { + Order("receive_success_time asc"). // 显式清除排序,移除默认的 ORDER BY + FindInBatches(&results, 1000, func(tx *gorm.DB, batch int) error { // tx.RowsAffected 提供当前批处理中记录的计数(the count of records in the current batch) // 'batch' 变量表示当前批号(the current batch number) // 返回 error 将阻止更多的批处理 @@ -163,6 +209,61 @@ func (p *OrderRepoImpl) FindInBatches(ctx context.Context, req *bo.FindInBatches return nil } +func (p *OrderRepoImpl) FindRetryQuery(ctx context.Context, req *do.RetryQueryNotice, fun func(ctx context.Context, rows []*bo.OrderBo) error) error { + + statusArr := []uint8{ + vo.OrderStatusSuccess.GetValue(), + vo.OrderStatusUse.GetValue(), + vo.OrderStatusExpired.GetValue(), + } + + tx := p.DB(ctx). + Where("`status` in (?)", statusArr). + Where("activity_id = ''") + + if req.ProductNo != "" { + tx = tx.Where("product_no = ?", req.ProductNo) + } + + if req.ReceiveSuccessStartTime != "" { + tx = tx.Where("receive_success_time > ?", req.ReceiveSuccessStartTime) + } + if req.ReceiveSuccessEndTime != "" { + tx = tx.Where("receive_success_time <= ?", req.ReceiveSuccessEndTime) + } + + if req.ProductNo != "" { + tx = tx.Where("product_no = ?", req.ProductNo) + } + + if req.OrderNos != nil { + tx = tx.Where("order_no IN (?)", req.OrderNos) + } + + if req.OutBizNos != nil { + tx = tx.Where("out_biz_no IN (?)", req.OutBizNos) + } + + if req.VoucherNos != nil { + tx = tx.Where("voucher_no IN (?)", req.VoucherNos) + } + + var results = make([]*model.Order, 0) + + tx.Order("receive_success_time asc") // 显式清除排序,移除默认的 ORDER BY + + result := tx.FindInBatches(&results, 1000, func(tx *gorm.DB, batch int) error { + + return fun(ctx, p.ToBos(results)) + }) + + if result.Error != nil { + return result.Error + } + + return nil +} + func (p *OrderRepoImpl) Create(ctx context.Context, req *bo.OrderBo) (*bo.OrderBo, error) { now := time.Now() @@ -274,22 +375,6 @@ func (p *OrderRepoImpl) GetByCouponId(ctx context.Context, merchantNo, batchNo, return p.ToBo(info), nil } -func (this *OrderRepoImpl) GetByTransactionId(ctx context.Context, stockCreatorMchId, stockID, transactionId string) (*bo.OrderBo, error) { - row := &model.Order{} - - tx := this.DB(ctx).Where(model.Order{MerchantNo: stockCreatorMchId, BatchNo: stockID, TransactionId: transactionId}).First(&row) - - if tx.Error != nil { - return nil, tx.Error - } - - if tx.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return this.ToBo(row), nil -} - func (p *OrderRepoImpl) Ing(ctx context.Context, id uint64) error { now := time.Now() @@ -410,7 +495,48 @@ func (p *OrderRepoImpl) Used(ctx context.Context, id uint64) error { return nil } -func (p *OrderRepoImpl) NotifyUsed(ctx context.Context, id uint64, transactionId string) error { +func (p *OrderRepoImpl) MultiLastUsed(ctx context.Context, id uint64, lastUseTime time.Time) error { + now := time.Now() + + tx := p.DB(ctx). + Where(model.Order{ + ID: id, + }). + Updates(model.Order{ + Remark: "核销", + LastUseTime: &lastUseTime, + UpdateTime: &now, + }) + + if tx.Error != nil { + return fmt.Errorf("update db fail %w", tx.Error) + } + + return nil +} + +func (p *OrderRepoImpl) MultiOverUsed(ctx context.Context, id uint64, lastUseTime time.Time, remark string) error { + now := time.Now() + + tx := p.DB(ctx). + Where(model.Order{ + ID: id, + }). + Updates(model.Order{ + Status: vo.OrderStatusUse.GetValue(), + Remark: remark, + LastUseTime: &lastUseTime, + UpdateTime: &now, + }) + + if tx.Error != nil { + return fmt.Errorf("update db fail %w", tx.Error) + } + + return nil +} + +func (p *OrderRepoImpl) NotifyUsed(ctx context.Context, id uint64, transactionId string, lastUseTime time.Time) error { now := time.Now() tx := p.DB(ctx). @@ -421,7 +547,7 @@ func (p *OrderRepoImpl) NotifyUsed(ctx context.Context, id uint64, transactionId Status: vo.OrderStatusUse.GetValue(), TransactionId: transactionId, Remark: "微信回调核销", - LastUseTime: &now, + LastUseTime: &lastUseTime, UpdateTime: &now, }) diff --git a/internal/data/repoimpl/order_bak.go b/internal/data/repoimpl/order_bak.go index 6509075..49c72d9 100644 --- a/internal/data/repoimpl/order_bak.go +++ b/internal/data/repoimpl/order_bak.go @@ -2,9 +2,15 @@ package repoimpl import ( "context" + "errors" + "fmt" "gorm.io/gorm" + "time" + err2 "voucher/api/err" "voucher/internal/biz/bo" + "voucher/internal/biz/do" "voucher/internal/biz/repo" + "voucher/internal/biz/vo" "voucher/internal/data" "voucher/internal/data/model" ) @@ -61,3 +67,142 @@ func (p *OrderBakRepoImpl) SpecifyFindInBatches(ctx context.Context, req *bo.Fin return nil } + +func (p *OrderBakRepoImpl) FindRetryQuery(ctx context.Context, req *do.RetryQueryNotice, fun func(ctx context.Context, rows []*bo.OrderBo) error) error { + + statusArr := []uint8{ + vo.OrderStatusSuccess.GetValue(), + vo.OrderStatusUse.GetValue(), + vo.OrderStatusExpired.GetValue(), + } + + tx := p.DB(ctx). + Where("`status` in (?)", statusArr). + Where("activity_id = ''") + + if req.ProductNo != "" { + tx = tx.Where("product_no = ?", req.ProductNo) + } + + if req.ReceiveSuccessStartTime != "" { + tx = tx.Where("receive_success_time > ?", req.ReceiveSuccessStartTime) + } + if req.ReceiveSuccessEndTime != "" { + tx = tx.Where("receive_success_time <= ?", req.ReceiveSuccessEndTime) + } + + if req.ProductNo != "" { + tx = tx.Where("product_no = ?", req.ProductNo) + } + + if req.OrderNos != nil { + tx = tx.Where("order_no IN (?)", req.OrderNos) + } + + if req.OutBizNos != nil { + tx = tx.Where("out_biz_no IN (?)", req.OutBizNos) + } + + if req.VoucherNos != nil { + tx = tx.Where("voucher_no IN (?)", req.VoucherNos) + } + + var results = make([]*model.OrderBak, 0) + + tx.Order("receive_success_time asc") // 显式清除排序,移除默认的 ORDER BY + + result := tx.FindInBatches(&results, 1000, func(tx *gorm.DB, batch int) error { + + return fun(ctx, p.ToBos(results)) + }) + + if result.Error != nil { + return result.Error + } + + return nil +} + +func (p *OrderBakRepoImpl) GetByID(ctx context.Context, id uint64) (*bo.OrderBo, error) { + info := &model.OrderBak{} + + tx := p.DB(ctx).Where(model.OrderBak{ID: id}).First(&info) + + if tx.Error != nil { + + if errors.Is(tx.Error, gorm.ErrRecordNotFound) { + return nil, err2.ErrorDbNotFound("订单数据不存在") + } + + return nil, fmt.Errorf("order db fail %w", tx.Error) + } + + if tx.RowsAffected == 0 { + return nil, err2.ErrorDbNotFound("订单数据不存在") + } + + return p.ToBo(info), nil +} + +func (p *OrderBakRepoImpl) Used(ctx context.Context, id uint64) error { + now := time.Now() + + tx := p.DB(ctx). + Where(model.OrderBak{ + ID: id, + }). + Updates(model.OrderBak{ + Status: vo.OrderStatusUse.GetValue(), + Remark: "核销", + LastUseTime: &now, + UpdateTime: &now, + }) + + if tx.Error != nil { + return fmt.Errorf("update db fail %w", tx.Error) + } + + return nil +} + +func (p *OrderBakRepoImpl) Expired(ctx context.Context, id uint64) error { + now := time.Now() + + tx := p.DB(ctx). + Where(model.OrderBak{ + ID: id, + }). + Updates(model.OrderBak{ + Status: vo.OrderStatusExpired.GetValue(), + Remark: "过期", + UpdateTime: &now, + }) + + if tx.Error != nil { + return fmt.Errorf("update db fail %w", tx.Error) + } + + return nil +} + +func (p *OrderBakRepoImpl) Available(ctx context.Context, id uint64) error { + now := time.Now() + + tx := p.DB(ctx). + Where(model.OrderBak{ + ID: id, + Status: vo.OrderStatusUse.GetValue(), + }). + Updates(model.OrderBak{ + Status: vo.OrderStatusSuccess.GetValue(), + Remark: "重置为成功,领取成功时间重置", + ReceiveSuccessTime: &now, // 领取成功时间重置 + UpdateTime: &now, + }) + + if tx.Error != nil { + return fmt.Errorf("update db fail %w", tx.Error) + } + + return nil +} diff --git a/internal/data/repoimpl/product.go b/internal/data/repoimpl/product.go index bad4723..93e3592 100644 --- a/internal/data/repoimpl/product.go +++ b/internal/data/repoimpl/product.go @@ -104,19 +104,42 @@ func (r *ProductRepoImpl) GetByBatchNo(ctx context.Context, batchNo string) (*bo tx := db.Where(model.Product{BatchNo: batchNo}).First(&item) if tx.Error != nil { - if errors.Is(tx.Error, gorm.ErrRecordNotFound) { - return nil, err2.ErrorDbNotFound("商品数据不存在") - } - return nil, fmt.Errorf("product db fail %w", tx.Error) + return nil, tx.Error } if tx.RowsAffected == 0 { - return nil, err2.ErrorDbNotFound("商品数据不存在") + return nil, gorm.ErrRecordNotFound } return r.ToBo(item), nil } +func (r *ProductRepoImpl) GetByMchStockId(ctx context.Context, mchId, stockId string) (*bo.ProductBo, error) { + + var item *model.Product + + tx := r.db.DB(ctx).Where(model.Product{MchId: mchId, BatchNo: stockId}).First(&item) + + if tx.Error != nil { + return nil, tx.Error + } + + if tx.RowsAffected == 0 { + return nil, gorm.ErrRecordNotFound + } + + return r.ToBo(item), nil +} + +func (r *ProductRepoImpl) DelCacheByProductNo(ctx context.Context, productNo string) error { + + c := vo.ProductQueryKey.BuildCache([]string{productNo}) + + _, _ = r.rdb.Rdb.Del(ctx, c.Key).Result() + + return nil +} + func (r *ProductRepoImpl) GetByProductNo(ctx context.Context, productNo string) (*bo.ProductBo, error) { c := vo.ProductQueryKey.BuildCache([]string{productNo}) diff --git a/internal/data/repoimpl/provider_set.go b/internal/data/repoimpl/provider_set.go index 9fd0991..dc58fc5 100644 --- a/internal/data/repoimpl/provider_set.go +++ b/internal/data/repoimpl/provider_set.go @@ -11,4 +11,6 @@ var ProviderRepoImplSet = wire.NewSet( NewOrderNotifyRepoImpl, NewWechatNotifyRegisterTagRepoImpl, NewOrderBakRepoImpl, + NewMultiNotifyDataRepoImpl, + NewMultiNotifyLogRepoImpl, ) diff --git a/internal/data/wechatrepoimpl/bank_multi_activity.go b/internal/data/wechatrepoimpl/bank_multi_activity.go index 2e0fc0c..7691239 100644 --- a/internal/data/wechatrepoimpl/bank_multi_activity.go +++ b/internal/data/wechatrepoimpl/bank_multi_activity.go @@ -1,11 +1,16 @@ package wechatrepoimpl import ( + "context" + "encoding/json" "errors" "fmt" + "github.com/go-kratos/kratos/v2/log" "github.com/wechatpay-apiv3/wechatpay-go/core" + "net/http" err2 "voucher/api/err" "voucher/internal/biz/bo" + "voucher/internal/biz/businesserr" "voucher/internal/biz/wechatrepo" "voucher/internal/conf" "voucher/internal/data" @@ -43,11 +48,20 @@ func (w *BankMultiActivityImpl) Order(order *bo.OrderBo) (couponId string, err e var e *utils.ApiException if errors.As(err, &e) { - apiErr, err3 := marketing.BuildErr(e.Body()) - if err3 != nil { - return "", fmt.Errorf("ApiException analysis err: %+v", err3) + // 格式:{"code":"INVALID_REQUEST","message":"对应单号已超出重试期;请查单确认后决定是否换单请求"} + var beer *businesserr.BusinessErr + if err = json.Unmarshal(e.Body(), &beer); err != nil { + log.Errorf("微信错误返回body解析报错,body:%s,err:%s", string(e.Body()), err.Error()) + return "", err2.ErrorWechatFAIL(fmt.Sprintf("微信错误返回内容解析错误:%s", err.Error())) } - return "", err2.ErrorWechatFAIL("%s-%s", apiErr.Code, apiErr.Message) + + //apiErr, err3 := marketing.BuildErr(e.Body()) + //if err3 != nil { + // return "", fmt.Errorf("ApiException analysis err: %+v", err3) + //} + //return "", err2.ErrorWechatFAIL("%s-%s", apiErr.Code, apiErr.Message) + + return "", beer } return "", err @@ -55,3 +69,66 @@ func (w *BankMultiActivityImpl) Order(order *bo.OrderBo) (couponId string, err e return *resp.CouponId, nil } + +func (w *BankMultiActivityImpl) Notify(ctx context.Context, mchId string, headers *http.Header, respBody []byte) (*bo.WechatVoucherNotifyBo, error) { + + t, err := w.wx.Get(mchId) + if err != nil { + return nil, err + } + + body, decodeBodyStr, err := t.Notify(ctx, headers, respBody) + if err != nil { + return nil, err + } + + var plainText bo.PlainText + if err = json.Unmarshal([]byte(decodeBodyStr), &plainText); err != nil { + return nil, fmt.Errorf("plainText json.Unmarshal error: %v", err) + } + + return &bo.WechatVoucherNotifyBo{ + ID: body.Id, + CreateTime: body.CreateTime, + ResourceType: body.ResourceType, + EventType: body.EventType, + Summary: body.Summary, + OriginalType: body.Resource.OriginalType, + AssociatedData: body.Resource.AssociatedData, + PlainText: plainText, + }, nil +} + +func (w *BankMultiActivityImpl) DecodeBody(ctx context.Context, mchId string, respBody []byte) (*bo.WechatVoucherNotifyBo, error) { + + t, err := w.wx.Get(mchId) + if err != nil { + return nil, err + } + + var body utils.WxNotifyBody + if err = json.Unmarshal(respBody, &body); err != nil { + return nil, err + } + + decryptedText, err := t.DecodeBody(&body) + if err != nil { + return nil, err + } + + var plainText bo.PlainText + if err = json.Unmarshal([]byte(decryptedText), &plainText); err != nil { + return nil, fmt.Errorf("plainText json.Unmarshal error: %v", err) + } + + return &bo.WechatVoucherNotifyBo{ + ID: body.Id, + CreateTime: body.CreateTime, + ResourceType: body.ResourceType, + EventType: body.EventType, + Summary: body.Summary, + OriginalType: body.Resource.OriginalType, + AssociatedData: body.Resource.AssociatedData, + PlainText: plainText, + }, nil +} diff --git a/internal/data/wechatrepoimpl/cpn.go b/internal/data/wechatrepoimpl/cpn.go index f40cbfa..367162c 100644 --- a/internal/data/wechatrepoimpl/cpn.go +++ b/internal/data/wechatrepoimpl/cpn.go @@ -9,14 +9,18 @@ import ( "github.com/wechatpay-apiv3/wechatpay-go/services/cashcoupons" "io" "net/http" + "strings" "time" err2 "voucher/api/err" "voucher/internal/biz/bo" + "voucher/internal/biz/businesserr" "voucher/internal/biz/vo" "voucher/internal/biz/wechatrepo" "voucher/internal/conf" "voucher/internal/data" + "voucher/internal/pkg/helper" "voucher/internal/pkg/request" + "voucher/internal/pkg/supplier/qixing" ) // CpnRepoImpl . @@ -52,18 +56,19 @@ func (c *CpnRepoImpl) GetClient(ctx context.Context) (*core.Client, error) { func (c *CpnRepoImpl) bodyErr(_ context.Context, result *core.APIResult) error { + // // 格式:{"code":"INVALID_REQUEST","message":"对应单号已超出重试期;请查单确认后决定是否换单请求"} bodyBytes, err := io.ReadAll(result.Response.Body) if err != nil { return err2.ErrorWechatFAIL(fmt.Sprintf("读取微信错误返回body报错:%s", err.Error())) } - var errBody ErrBody - if err = json.Unmarshal(bodyBytes, &errBody); err != nil { + var beer *businesserr.BusinessErr + if err = json.Unmarshal(bodyBytes, &beer); err != nil { log.Errorf("微信错误返回body解析报错,body:%s,err:%s", string(bodyBytes), err.Error()) return err2.ErrorWechatFAIL(fmt.Sprintf("微信错误返回内容解析错误:%s", err.Error())) } - return errBody.GetWechatError() + return beer } func (c *CpnRepoImpl) Order(ctx context.Context, order *bo.OrderBo) (string, error) { @@ -97,12 +102,85 @@ func (c *CpnRepoImpl) Order(ctx context.Context, order *bo.OrderBo) (string, err return *resp.CouponId, nil } -func (c *CpnRepoImpl) Query(ctx context.Context, orderWechat *bo.OrderBo) (vo.OrderStatus, error) { +func (c *CpnRepoImpl) Query(ctx context.Context, order *bo.OrderBo) (vo.OrderStatus, error) { + + // todo 确认下,多笔立减金用普通立减金的接口查询也能查,结果是准确的吗 + + // 福州启蒙 - 启星 + if order.MerchantNo == "1715349578" { + //return c.QxQuery(ctx, order) + } + + return c.LsxdQuery(ctx, order) +} + +func (c *CpnRepoImpl) QxQuery(ctx context.Context, order *bo.OrderBo) (vo.OrderStatus, error) { + + if order.ActivityId == "" { + return 0, fmt.Errorf("商户号 %s 只支持多笔立减金查询", order.MerchantNo) + } + + b := qixing.QxQueryReq{ + OrderNo: order.OrderNo, + CouponId: order.VoucherNo, + OpenId: order.Account, + } + + var strToBeSigned strings.Builder + + kvRows := helper.SortStructJsonTag(b) + 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" + + b.Sign = helper.Md5(s) + + body, err := json.Marshal(b) + if err != nil { + return 0, err + } + + isSuccess := func(code int) bool { + return code == http.StatusOK + } + + h := http.Header{ + "Content-Type": []string{"application/json"}, + } + + _, body, err = request.Post( + ctx, + c.bc.WechatNotifyMQ.RegisterTagUrl, + body, + request.WithHeaders(h), + request.WithStatusCodeFunc(isSuccess), + request.WithTimeout(time.Second*10), + ) + if err != nil { + return 0, err + } + + var resp qixing.QxQueryResp + if err := json.Unmarshal(body, &resp); err != nil { + return 0, err + } + + // 统一返回状态类型 + return resp.Data.CouponState.GetStatus() +} + +func (c *CpnRepoImpl) LsxdQuery(ctx context.Context, order *bo.OrderBo) (vo.OrderStatus, error) { + + // 需要判断是多笔立减金查询还是普通立减金查询,此处需要更正查询方式 req := cashcoupons.QueryCouponRequest{ - CouponId: core.String(orderWechat.VoucherNo), - Appid: core.String(orderWechat.AppID), - Openid: core.String(orderWechat.Account), + CouponId: core.String(order.VoucherNo), + Appid: core.String(order.AppID), + Openid: core.String(order.Account), } client, err := c.GetClient(ctx) @@ -122,6 +200,12 @@ func (c *CpnRepoImpl) Query(ctx context.Context, orderWechat *bo.OrderBo) (vo.Or return 0, err } + cpnStatus := CpnStatus(*resp.Status) + + if cpnStatus.IsRevoked() { + + } + return CpnStatus(*resp.Status).GetStatus() } diff --git a/internal/data/wechatrepoimpl/cpn_code_test.go b/internal/data/wechatrepoimpl/cpn_code_test.go index ef2e8b0..00762b5 100644 --- a/internal/data/wechatrepoimpl/cpn_code_test.go +++ b/internal/data/wechatrepoimpl/cpn_code_test.go @@ -1,39 +1 @@ package wechatrepoimpl - -import ( - "errors" - errors2 "github.com/go-kratos/kratos/v2/errors" - "gorm.io/gorm" - "testing" -) - -func TestGetErrorByDescription(t *testing.T) { - e := &ErrBody{ - Code: "INVALID_REQUEST", - Message: "活动已结束或未激活", - } - t.Log(e.GetWechatError()) - - err := gorm.ErrRecordNotFound - - t.Log(errors.Is(err, gorm.ErrRecordNotFound)) -} - -func TestErr(t *testing.T) { - e := &ErrBody{ - Code: "INVALID_REQUEST", - Message: "活动已结束或未激活", - } - - se := errors2.FromError(e.GetWechatError()) - - t.Log(se.Reason) - t.Log(se.Message) - - e2 := errors.New("活动已结束或未激活") - se2 := errors2.FromError(e2) - - t.Log(se2.Reason) - t.Log(len(se2.Reason)) - t.Log(se2.Message) -} diff --git a/internal/data/wechatrepoimpl/cpn_status.go b/internal/data/wechatrepoimpl/cpn_status.go index 16351ee..eea61cf 100644 --- a/internal/data/wechatrepoimpl/cpn_status.go +++ b/internal/data/wechatrepoimpl/cpn_status.go @@ -13,12 +13,16 @@ const ( CpnStatusAvailable = "SENDED" CpnStatusUsed = "USED" CpnStatusExpired = "EXPIRED" + CpnStatusRevoked = "REVOKED" + CpnStatusRecover = "RECOVER" ) var CpnStatusTextMap = map[CpnStatus]string{ CpnStatusAvailable: "可用", CpnStatusUsed: "已实扣", CpnStatusExpired: "已过期", + CpnStatusRevoked: "已失效", + CpnStatusRecover: "已回收", } var CpnStatusMap = map[CpnStatus]vo.OrderStatus{ @@ -34,9 +38,13 @@ func (o CpnStatus) GetText() string { return "未知" } +func (o CpnStatus) IsRevoked() bool { + return o == CpnStatusRevoked +} + func (o CpnStatus) GetStatus() (vo.OrderStatus, error) { if resultStatus, ok := CpnStatusMap[o]; ok { return resultStatus, nil } - return 0, fmt.Errorf("CpnStatus[%s]未定义", o) + return 0, fmt.Errorf("CpnStatus[%s-%s]未定义", o, o.GetText()) } diff --git a/internal/data/wx.go b/internal/data/wx.go index fb9df64..6d268c0 100644 --- a/internal/data/wx.go +++ b/internal/data/wx.go @@ -24,6 +24,14 @@ func NewWx(c *conf.Bootstrap) (*Wx, error) { clients[c.Wechat.MchID] = client + for _, v := range c.WechatSubject { + cli, e := buildWechat(v) + if e != nil { + return nil, e + } + clients[v.MchID] = cli + } + return &Wx{Clients: clients}, nil } @@ -44,6 +52,33 @@ func (this *Wx) Get(mchId string) (*marketing.Marketing, error) { return nil, fmt.Errorf("微信调用client不存在[%s]", mchId) } +func buildWechat(wx *conf.WechatSubject) (*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, "wechat_private_key.pem"), // 商户API证书私钥文件路径,本地文件路径 + wx.WechatPayPublicKeyID, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 + fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径 + wx.MchApiV3Key, + ) + if err != nil { + return nil, err + } + + return &marketing.Marketing{MchConfig: cc}, nil +} + func buildWx(wx *conf.Wechat) (*marketing.Marketing, error) { dir, err := os.Getwd() @@ -62,6 +97,7 @@ func buildWx(wx *conf.Wechat) (*marketing.Marketing, error) { fmt.Sprintf("%s/%s", filePath, "wechat_private_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 diff --git a/internal/pkg/cmb/sm2_test.go b/internal/pkg/cmb/sm2_test.go index 511443f..fb166d9 100644 --- a/internal/pkg/cmb/sm2_test.go +++ b/internal/pkg/cmb/sm2_test.go @@ -186,14 +186,11 @@ func TestVerifyBody(t *testing.T) { } func TestDecryptBody(t *testing.T) { - //priKey := "f6a8d2f412e289686aba6a0f33cad1a64367d0ba012046ee0fbbefd3ffd675bd" - priKey := "8d39ff3d2559258c163f4510f082727f51531e1953ab203d5ab1ea4a6d94fd73" + priKey := "f6a8d2f412e289686aba6a0f33cad1a64367d0ba012046ee0fbbefd3ffd675bd" // 测试 + //priKey := "8d39ff3d2559258c163f4510f082727f51531e1953ab203d5ab1ea4a6d94fd73" - //content := "BAdcIauIjNx3LsrplpJiZoljE4hCiGHra6ulhgG1qL0tKcAeenX+Z9VaHfXLSdkji1fYBpdZiiI35R0vFtnXPXJCJdHsGbfbae+PzNznYQS3KM8/90Y/FIWzSoszfUiF6fAuv8I6v9kQuqHUTidHeHyICDoyvJ0nhbNyUyg85bAKd6TmkVX1MgXLQ81m|5KfR/5UkpVBEQv1dx+iJbojOykNRuDV8Gsy3QOIlRI+cZvafRRPUUG6eeixnPMumhOvyZwsSG/OBeg0U/lSlAepg12tXWcQ601wjgyLaKN1iMvb1DCtfnJFAm8EWAc2SLH3NQuyhxGe/jgCXvj0wGphh4vBUzm8la8i8Aij0BI5lfgU5OzglkKDln6zHN3vBHDqOurEh18eU6z1bfvNnDpzdwEcygcEIH/6lGiqVnGH+C2+QpcKeCnj5qKGFiuSC" + content := "BH666zulhEdX6Axd3+LUaPo0a6WU1ze2qKFrtu+pxq4EZAy1TPY1NQ0+53+WpscLsoksfjlpCOPVpSLQDKe1IHARFTgJSNgsOfvdBGIEoyDEDdMOc6JzONUuijKsfUTnQI+UsecoJOmRo4xtSloR0sql9FPqhxTn9quMrsJQVL/DNnol+vs8r6RsyLPL|ArPZsEB4BoGL5UX0BTb9vdyOuSt+cMf7msRcEIUUX6O/oOoCdQmxYIVsaLCgq9IkK6mP4O1oAiu6R8gfdsyHgIQSDtKiv7DRAwrI/b4UD4ka+BVVhJXkX0uYlqmA8yCrcmdM2zwp7c/v0qeEr0LnJhrm2tPwAEhLdWEDJskNsH/dQsVkWxf77Vj1OtLQjl+6Ir0RYzdEXlBxNGBv6Y0x2khQcbcTbVfcMFJ3zoGcCEU75UrDLS/ph73t/BM2dICakqL+ymlKiqMm/+K77B4pdYbJ2SpiUdydv4wUzNLPsx2mhuh8KhuyEo1DI8Fzjqd3TiuXiIZISyxaXJ3aV9NdxdYVkZ4j5iLpYdL7GqAxBtdLLK/J6KtqzW+3rwM3L7alxbXHVdcgoxVZanunisaNWPsn4V5BUoL8halT3/smwZM=" //content := "BDUuuPClIlUKDJdpHtNBIU6u5JTetrVG53AfKDSrhah9Q0QZWAj3K5pZF8G/HFtzj/KvbrHfP/gnHri0L7L91HaKYYc1vqy/q8Z69v7MEiIWL2LeLOsc2b0cHmnt7Qey5aZzYVbZJJNhus2gvhahGwOPpPL50JFC8IqAlU4+E/kUBgx+RgzAIkLLs8Se|k1WOt2Eb+xkxSobw/1DaLblguznhnsk1ga87sfqrrPATRuTyszzdVrRtCUuUHRKJ+vQwlZAR1ypkBC1vPMdt8zccI/CeNof+4Ap23enbQwTJQ4KRvij4kbJd6ycY97B+vHnI+oSnwfmjK0EWNUeRrCr2Uau7yxmzlFwJHprbgPZpHVuUjAzMIXNixOfxSTdc9dL+j3/tR7+yookvd9W3hNm96XZFPTheI0FqUKkbKWUUcVs4lC1/yEgjwXFcD4NM" - //content := "BDqANzEC9vq82FTu9wR89Ou/8o2J2vD7yeoExb1Uxl2PSA9EsvI7YQNGSkSdxoloqRBn4vIrA8Uh+FmWtJ9wbFEEtbC+Epj8qHWN8S+lH9JEnvJhPhEtMgmHeuGDtvzHMACvc7KoBmJ/6XZIoEyLeRgkJnCTVVxnD78KMJ4eXzj7C2ErkISJ5r3qcL9t|cPw/uy87PW74aeU1RwGwUsYXZlXYkG0m+BoO/3qT4rLSIWsHvBN76yQJlTk89aa9VALdtwcU7H0q7ivCLZ9uflB6YFkg5QRQoj1b2AFFU4TPkpgAdCBAKs5+6z9cwDn+QQyuamKAnp1wJfMj7Ksa3HxLwCxTh/w2dF6LuruLFWZstMK1g9ID7tVTPqc05VNmktmNhc97BYuIH66xa0ZFgr+0i/3hFMx0hNTOau5UWFM6v4Lb1iM/v9ggM5ZvAheUspfgvRUhpD2TZvgyneWxqg==" - //content := "BOk+pytaf5faPeISKp/rzspV2ngCr/RuHe3lTTGv+0csaKh3y/x/e6njEbpYpQXCcYTI4eN9pjKMyU9ByA1hsfmiCSU4ziUCu5+ltxDFt/FHuhspBwIYRkVCQM9WefydXgGEuXBd0S+yB6BhBMqJgSUuyjxyF5AecdFoB9AoRyslbqSflqn2cctw5pIx|yvTLMRUfjO+9XV4ilxNBMD9jLwA1PYdthta5uhOOSzaJVOdNDhC+aAltyrLrZkBIUOrPZX7U3RtJVLnsk0y+t8/di+XxriZtnZZ0vTJC0q5tAONN9IE7s9clGGclX5dXYGV4jhP70tFzSeTlWsviLAs9sCvsuIloKEGLJdDvJ53vxiYskX3+pOF0uvOkAlJb70pfOHXsIzSAEhFC88O6o9AR4BRa18ex3V+CqN0CkFUqVazvxoyPFlhzxRXZyBkt" - content := "BOk+pytaf5faPeISKp/rzspV2ngCr/RuHe3lTTGv+0csaKh3y/x/e6njEbpYpQXCcYTI4eN9pjKMyU9ByA1hsfmiCSU4ziUCu5+ltxDFt/FHuhspBwIYRkVCQM9WefydXgGEuXBd0S+yB6BhBMqJgSUuyjxyF5AecdFoB9AoRyslbqSflqn2cctw5pIx|yvTLMRUfjO+9XV4ilxNBMD9jLwA1PYdthta5uhOOSzaJVOdNDhC+aAltyrLrZkBIUOrPZX7U3RtJVLnsk0y+t8/di+XxriZtnZZ0vTJC0q5tAONN9IE7s9clGGclX5dXYGV4jhP70tFzSeTlWsviLAs9sCvsuIloKEGLJdDvJ53vxiYskX3+pOF0uvOkAlJb70pfOHXsIzSAEhFC88O6o9AR4BRa18ex3V+CqN0CkFUqVazvxoyPFlhzxRXZyBkt" rs, err := DecryptBody(&Decrypts{content, priKey}) if err != nil { diff --git a/internal/pkg/helper/ip.go b/internal/pkg/helper/ip.go new file mode 100644 index 0000000..6600bef --- /dev/null +++ b/internal/pkg/helper/ip.go @@ -0,0 +1,65 @@ +package helper + +import ( + "net" + "strings" + + "github.com/go-kratos/kratos/v2/transport/http" +) + +// GetClientIP 获取客户端真实 IP +func GetClientIP(ctx http.Context) string { + + // 检查 X-Forwarded-For 头(多个代理时格式为 "client, proxy1, proxy2") + if xff := ctx.Header().Get("X-Forwarded-For"); xff != "" { + ips := strings.Split(xff, ",") + for _, ip := range ips { + ip = strings.TrimSpace(ip) + if ip != "" { + // 验证是否为合法 IP + if isValidIP(ip) { + return ip + } + } + } + } + + // 检查 X-Real-IP 头 + if realIP := ctx.Header().Get("X-Real-IP"); realIP != "" { + if isValidIP(realIP) { + return realIP + } + } + + // 检查 X-Forwarded + if forwarded := ctx.Header().Get("X-Forwarded"); forwarded != "" { + // 格式可能为 "for=client-ip;host=example.com;proto=https" + parts := strings.Split(forwarded, ";") + for _, part := range parts { + part = strings.TrimSpace(part) + if strings.HasPrefix(part, "for=") { + ip := strings.TrimPrefix(part, "for=") + ip = strings.Trim(ip, `"`) // 可能被引号包围 + if isValidIP(ip) { + return ip + } + } + } + } + + // 直接从 RemoteAddr 获取 + remoteAddr := ctx.Request().RemoteAddr + if ip, _, err := net.SplitHostPort(remoteAddr); err == nil { + if isValidIP(ip) { + return ip + } + } + + return "" +} + +// 验证是否为合法 IP +func isValidIP(ip string) bool { + parsedIP := net.ParseIP(ip) + return parsedIP != nil +} diff --git a/internal/pkg/helper/sort_struct.go b/internal/pkg/helper/sort_struct.go new file mode 100644 index 0000000..7661391 --- /dev/null +++ b/internal/pkg/helper/sort_struct.go @@ -0,0 +1,53 @@ +package helper + +import ( + "fmt" + "reflect" + "sort" +) + +type KeyValue struct { + Key string + Value string +} + +func SortStruct(data any) []KeyValue { + v := reflect.ValueOf(data).Elem() + t := v.Type() + var kv []KeyValue + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + key := LowercaseFirstLetter(t.Field(i).Name) + value := fmt.Sprintf("%v", field.Interface()) + kv = append(kv, KeyValue{Key: key, Value: value}) + } + sort.SliceStable(kv, func(i, j int) bool { + return kv[i].Key < kv[j].Key + }) + return kv +} + +func SortStructJsonTag(data any) []KeyValue { + // 获取 data 结构体的类型和值 + dataType := reflect.TypeOf(data).Elem() + dataValue := reflect.ValueOf(data).Elem() + + var kv []KeyValue + for i := 0; i < dataType.NumField(); i++ { + field := dataType.Field(i) + // 获取字段的值 + fieldValue := dataValue.FieldByName(field.Name).Interface() + // 获取 JSON 字段名 + jsonTagName := field.Tag.Get("json") + if jsonTagName != "" { + kv = append(kv, KeyValue{Key: jsonTagName, Value: fmt.Sprintf("%v", fieldValue)}) + } + } + + // 排序 + sort.SliceStable(kv, func(i, j int) bool { + return kv[i].Key < kv[j].Key + }) + + return kv +} diff --git a/internal/pkg/helper/utils_test.go b/internal/pkg/helper/utils_test.go index b8d2096..42278ee 100644 --- a/internal/pkg/helper/utils_test.go +++ b/internal/pkg/helper/utils_test.go @@ -1,6 +1,7 @@ package helper import ( + "encoding/base64" "fmt" "testing" "time" @@ -43,6 +44,63 @@ func queryUsed(useNum *int) { } func TestMd5(t *testing.T) { - s := Md5(`{"product_no":"","start_time":"2025-04-20 09:00:00","end_time":"2025-05-01 00:00:00"}`) + + jsonStr := `{"content":"1","timestamp":1765447477945,"ciphertext":"77CAC2FCFDEBAC6665025A1B81E3BBF9"}` + + ciphertext := Md5(jsonStr) + t.Log(ciphertext) +} + +func Test_DecodeString(t *testing.T) { + wxNotifyData, _ := base64.StdEncoding.DecodeString("ewogICJpZCI6ICI0YWIyNjk5ZC1lOTFkLTU0NjAtOTgxMC0yNWZkNmQ0YzY5YTUiLAogICJjcmVhdGVfdGltZSI6ICIyMDI1LTEyLTA4VDE3OjU0OjI0KzA4OjAwIiwKICAicmVzb3VyY2VfdHlwZSI6ICJlbmNyeXB0LXJlc291cmNlIiwKICAiZXZlbnRfdHlwZSI6ICJDT1VQT04uVVNFIiwKICAic3VtbWFyeSI6ICLku6Pph5HliLjmoLjplIDpgJrnn6UiLAogICJvcmlnaW5hbF90eXBlIjogImNvdXBvbiIsCiAgImFzc29jaWF0ZWRfZGF0YSI6ICJjb3Vwb24iLAogICJwbGFpbl90ZXh0IjogewogICAgInN0b2NrX2NyZWF0b3JfbWNoaWQiOiAiMTY1MjQ2NTU0MSIsCiAgICAic3RvY2tfaWQiOiAiMjEzODY0ODQiLAogICAgImNvdXBvbl9pZCI6ICIxNDIzODgzNTQ5OTQiLAogICAgImNvdXBvbl9uYW1lIjogIumTtuihjOWNoeWkmueslOeri+WHjyIsCiAgICAiZGVzY3JpcHRpb24iOiAiIiwKICAgICJzdGF0dXMiOiAiU0VOREVEIiwKICAgICJjcmVhdGVfdGltZSI6ICIyMDI1LTEyLTA4VDE3OjUwOjQ4KzA4OjAwIiwKICAgICJjb3Vwb25fdHlwZSI6ICJOT1JNQUwiLAogICAgIm5vX2Nhc2giOiBmYWxzZSwKICAgICJzaW5nbGVpdGVtIjogZmFsc2UsCiAgICAiYnVzaW5lc3NfdHlwZSI6ICIiLAogICAgImNvbnN1bWVfaW5mb3JtYXRpb24iOiB7CiAgICAgICJjb25zdW1lX3RpbWUiOiAiMjAyNS0xMi0wOFQxNzo1NDoyNCswODowMCIsCiAgICAgICJjb25zdW1lX21jaGlkIjogIjEyNzQ5Mzg2MDEiLAogICAgICAidHJhbnNhY3Rpb25faWQiOiAiNDIwMDAwMjk5NjIwMjUxMjA4MzA2MzA1MTgzNCIsCiAgICAgICJjb25zdW1lX2Ftb3VudCI6IDE2CiAgICB9CiAgfQp9") + t.Log(string(wxNotifyData)) +} + +func TestLength(t *testing.T) { + + jsonStr := `{ + "id": "4ab2699d-e91d-5460-9810-25fd6d4c69a5", + "create_time": "2025-12-08T17:54:24+08:00", + "resource_type": "encrypt-resource", + "event_type": "COUPON.USE", + "summary": "代金券核销通知", + "original_type": "coupon", + "associated_data": "coupon", + "plain_text": { + "stock_creator_mchid": "1652465541", + "stock_id": "21386484", + "coupon_id": "142388354994", + "coupon_name": "银行卡多笔立减", + "description": "", + "status": "SENDED", + "create_time": "2025-12-08T17:50:48+08:00", + "coupon_type": "NORMAL", + "no_cash": false, + "singleitem": false, + "business_type": "", + "consume_information": { + "consume_time": "2025-12-08T17:54:24+08:00", + "consume_mchid": "1274938601", + "transaction_id": "4200002996202512083063051834", + "consume_amount": 16 + } + } +}` + s := len(jsonStr) t.Log(s) } + +func Test_Time(t *testing.T) { + + strTime := "2026-03-27 10:33:20" + tt, _ := time.Parse("2006-01-02 15:04:05", strTime) + t.Log(tt.Format("2006-01-02 15:04:05.000")) + + //for i := 0; i < 20; i++ { + // formatTime := time.Now().Format("2006-01-02 15:04:05.999") + // t.Log(formatTime) + // formatTime2 := time.Now().Format("2006-01-02 15:04:05.000") + // t.Log(formatTime2) + // time.Sleep(1 * time.Second) + //} +} diff --git a/internal/pkg/request/request_test.go b/internal/pkg/request/request_test.go index 4f13710..fa07f27 100644 --- a/internal/pkg/request/request_test.go +++ b/internal/pkg/request/request_test.go @@ -2,6 +2,8 @@ package request import ( "context" + "encoding/json" + "fmt" "net/http" "net/url" "testing" @@ -24,8 +26,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,11 +45,12 @@ func Test_RequestHeaders(t *testing.T) { return } - t.Logf("响应体:", string(respBody)) - t.Logf("响应头:", respHeader) + t.Logf("响应体:%s", string(respBody)) + t.Logf("响应头:%+v", respHeader) } func Test_RequestStatusCode(t *testing.T) { + uri := "http://example.com/api/update" body := []byte("update data") @@ -61,6 +64,43 @@ func Test_RequestStatusCode(t *testing.T) { return } - t.Logf("响应体:", string(respBody)) - t.Logf("响应头:", respHeader) + t.Logf("响应体:%s", string(respBody)) + t.Logf("响应头:%+v", respHeader) +} + +func Test_WxNotifyRequest(t *testing.T) { + + uri := "https://gateway.dev.cdlsxd.cn/voucher/v1/notify/1100040695" + //uri := "https://voucher.86698.cn/voucher/v1/notify/1100040695" + + headerBytes := []byte(`{"Accept":["*/*"],"Cache-Control":["no-cache"],"Connection":["close"],"Content-Length":["1113"],"Content-Type":["application/json"],"Pragma":["no-cache"],"User-Agent":["Mozilla/4.0"],"Wechatpay-Nonce":["TD238pwsm3x0rYo57A8Z27Qa3AFDmToI"],"Wechatpay-Serial":["PUB_KEY_ID_0111000406952026032500382251001000"],"Wechatpay-Signature":["a64Z4Lngoz61OhhpG82g/rv8tYV0KmnE7g5KA4hvj7bY6XKSdQaifajXnLQHxA70owmd1kGA6wY5ZxDCvxnNHRjiNP4f68Ii7swL0TCfcU3aLpkLlmPM4wtzSh4YK1cIg4z/mqpxLDkxUky2vp4BvmGx6oKjK5Yv/emFerS2rWWdM2tTdi5Rv1rAGz/osqmrW8d+ZsLlGKHZ6wh7GUmNqt/qF+/kx+GB1jpzCgn8aUQqNvfnbtFcs9SKMQqm+DmBUO85IHL2WCQsuQ6cWpTrRCg5TGj0M0b8TEVHLbX0eBxtUyyGtdwuQbVbEzqCbwLs234YLG1Koq8lxZx6Nib1OQ=="],"Wechatpay-Signature-Type":["WECHATPAY2-SHA256-RSA2048"],"Wechatpay-Timestamp":["1774864935"],"X-Forwarded-For":["121.51.58.174, 172.29.42.89"]}`) + bodyBytes := []byte(`{"id":"ab4dbb6c-d9e9-5d2f-9f22-9697dfe2a58e","create_time":"2026-03-30T18:02:15+08:00","resource_type":"encrypt-resource","event_type":"COUPON.USE","summary":"代金券核销通知","resource":{"original_type":"coupon","algorithm":"AEAD_AES_256_GCM","ciphertext":"lsBBeh/DxRiecPLgtFyZov6tsCA38vWQ4QZxZouyp48mr3/kP3jSu/l2Sl1Rr59a4F0iIFSxuc7p6+/ovX11Ol7i+gbJmNlc+F3Yfo/bb8zyt/ubys4Ep0Or3YEvZr+h4G4k8rPn0iTYlzuY2hBcfLAazt/qTSz87aNmpBz3XN1CJL+5C0LffWdd5j1zjxfY2AYiUwtSuzo2cRYrxberxyeL9SeHJnqc+8o0njB6fyOJ4q5D9SyPqSNe228tW2xw0/d9QNWtzIlwQhyCwuhdXh0GBSWO7jdEmD3tQCIOk3OgoooywBZFtfwUvMiy2/tohEKd9JIQi3OVV2ctUx+cBdJ7RIH2r8PFZO55PqIND9itDCraSbfHQpqlYfVLZMossa92U54khv9FOzy/RQJPut/YWFkC4PsXgtfPpXQI0l6M+if1AVX+h4XHKk+wCd3fQ37tAwpxNF9kCuLgCIzgmnWjxj8296z6lQkKFomDn6xLdv6ECtnNYGhItQqU6v122/klkeysURSn1qFSbkujFvByCEKSmasTHe1yLorvfAYl+YJVnvLKsBxjjlCiEdCjspTFlDG3m+N/DxsYpYegYxvzfrM2MhgjJwPBbuxxgz4KMtYyiOrHvMDg8qCA41fRkxO4JkSA0JP22Q7v6zXOcHgf/w5aNo6xyICUrg2AueY0UV2uDA4yF6hYTho+CfGLjRfdFCuDReBPlA2TcJKSEtAWJW25nFrqKY/C87xAiD99JPf+tg7h2HcDCzasnV8h/AGA3PSQf2ZX4TsuyRdyPQ+uQWSW","associated_data":"coupon","nonce":"c6H0W8tKIiJP"}}`) + + var headerMap http.Header + if err := json.Unmarshal(headerBytes, &headerMap); err != nil { + t.Error(fmt.Sprintf("解析 headers 失败: %v", err)) + return + } + + hc := &http.Client{ + Timeout: 5 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 1, // 最大空闲连接数 + MaxIdleConnsPerHost: 1, // 每个主机的最大空闲连接数 + IdleConnTimeout: 10 * time.Second, // 空闲连接超时时间 + }, + } + + isSuccess := func(code int) bool { + return code == http.StatusOK || code == http.StatusCreated + } + + respHeader, respBody, err := Post(context.Background(), uri, bodyBytes, WithHttpClient(hc), WithStatusCodeFunc(isSuccess), WithHeaders(headerMap)) + if err != nil { + t.Error(err) + return + } + + t.Logf("响应体:%s", string(respBody)) + t.Logf("响应头:%v", respHeader) } diff --git a/internal/pkg/supplier/qixing/qixing.go b/internal/pkg/supplier/qixing/qixing.go new file mode 100644 index 0000000..38d0489 --- /dev/null +++ b/internal/pkg/supplier/qixing/qixing.go @@ -0,0 +1,27 @@ +package qixing + +import ( + "github.com/go-playground/validator/v10" +) + +type QiXingRequestBo struct { + Content string `json:"content" validate:"required"` + Timestamp int64 `json:"timestamp" validate:"required"` + Ciphertext string `json:"ciphertext" validate:"required"` +} + +// QiXingResponse 响应结构体 {"msg":"SUCCESS"} / {"msg":"操作成功"} +type QiXingResponse struct { + Msg string `json:"msg"` +} + +func (c *QiXingRequestBo) Validate() error { + + if err := validator.New().Struct(c); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return err + } + } + + return nil +} diff --git a/internal/pkg/supplier/qixing/query.go b/internal/pkg/supplier/qixing/query.go new file mode 100644 index 0000000..6c6424f --- /dev/null +++ b/internal/pkg/supplier/qixing/query.go @@ -0,0 +1,59 @@ +package qixing + +import ( + "fmt" + "time" + "voucher/internal/biz/vo" +) + +type CouponState string + +const ( + CouponStateUnknown CouponState = "COUPON_STATE_UNKNOW" // 未知状态 + CouponStateSend CouponState = "COUPON_STATE_SEND" // 可用 + CouponStateUsed CouponState = "COUPON_STATE_USED" // 已实扣 + CouponStateExpired CouponState = "COUPON_STATE_EXPIRED" // 已过期 +) + +var CpnStatusMap = map[CouponState]vo.OrderStatus{ + CouponStateSend: vo.OrderStatusSuccess, + CouponStateUsed: vo.OrderStatusUse, + CouponStateExpired: vo.OrderStatusExpired, +} + +func (o CouponState) GetStatus() (vo.OrderStatus, error) { + if resultStatus, ok := CpnStatusMap[o]; ok { + return resultStatus, nil + } + return 0, fmt.Errorf("CpnStatus[%s]未定义", o) +} + +type QxQueryReq struct { + OrderNo string `json:"order_no"` + CouponId string `json:"coupon_id"` + OpenId string `json:"open_id"` + Sign string `json:"sign"` +} + +type QxQueryResp struct { + Code any `json:"code"` + Msg string `json:"msg"` + Data *struct { + StockCreatorMchID string `json:"stock_creator_mchid"` + StockID string `json:"stock_id"` + CouponID string `json:"coupon_id"` + CouponName string `json:"coupon_name"` + CouponState CouponState `json:"coupon_state"` + ReceiveTime time.Time `json:"receive_time"` + AvailableBeginTime time.Time `json:"available_begin_time"` + AvailableEndTime time.Time `json:"available_end_time"` + ActivityID string `json:"activity_id"` + MaxUseNumber int `json:"max_use_number"` + AvailableNumber int `json:"available_number"` + UsedNumber int `json:"used_number"` + UseAmountList struct { + UsedAmounts []string `json:"used_amounts"` + } `json:"use_amount_list"` + OpenID string `json:"openid"` + } `json:"data"` +} diff --git a/internal/pkg/wechat/srv/marketing/marketing.go b/internal/pkg/wechat/srv/marketing/marketing.go index 65203e0..ea67fd5 100644 --- a/internal/pkg/wechat/srv/marketing/marketing.go +++ b/internal/pkg/wechat/srv/marketing/marketing.go @@ -1,7 +1,9 @@ package marketing import ( + "context" "encoding/json" + "net/http" "net/url" "strings" "voucher/internal/pkg/wechat/srv" @@ -39,13 +41,13 @@ func (srv *Marketing) Send(openId string, req *SendReq) (response *SendResp, err } // Query @link https://pay.weixin.qq.com/doc/v3/merchant/4014569864 -func (srv *Marketing) Query(appid, openId, couponId string) (response *SendResp, err error) { +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) + uv.Set("appid", appId) path += "?" + uv.Encode() respBody, err := srv.Request2(utils.Host, utils.MethodGET, path, nil) @@ -59,3 +61,18 @@ func (srv *Marketing) Query(appid, openId, couponId string) (response *SendResp, return response, nil } + +func (srv *Marketing) Notify(_ context.Context, headers *http.Header, respBody []byte) (body *utils.WxNotifyBody, response string, err error) { + + wxNotifyBody, err := srv.GetNotifyBody(headers, respBody) + if err != nil { + return nil, "", err + } + + bizStr, err := srv.DecodeBody(wxNotifyBody) + if err != nil { + return nil, "", err + } + + return wxNotifyBody, bizStr, nil +} diff --git a/internal/pkg/wechat/utils/aes.go b/internal/pkg/wechat/utils/aes.go new file mode 100644 index 0000000..d95fb4c --- /dev/null +++ b/internal/pkg/wechat/utils/aes.go @@ -0,0 +1,87 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "errors" + "fmt" +) + +const ( + keyLengthByte = 32 // AES-256 密钥长度(字节) + authTagLengthByte = 16 // GCM 认证标签长度(字节) +) + +// AesUtil 用于微信支付 AES-256-GCM 解密的工具类 +type AesUtil struct { + aesKey []byte // 32字节的AES密钥 +} + +// NewAesUtil 创建AesUtil实例,验证密钥长度 +func NewAesUtil(aesKey string) (*AesUtil, error) { + if len(aesKey) != keyLengthByte { + return nil, errors.New("无效的ApiV3Key,长度应为32个字节") + } + return &AesUtil{aesKey: []byte(aesKey)}, nil +} + +// DecryptToString 解密 AEAD_AES_256_GCM 加密的数据 +// associatedData: 附加认证数据 +// nonceStr: 随机数(12字节) +// ciphertext: 加密后的密文(Base64编码) +func (a *AesUtil) DecryptToString(associatedData, nonceStr, ciphertext string) (string, error) { + // 1. Base64解码密文 + cipherBytes, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", fmt.Errorf("密文Base64解码失败: %w", err) + } + + // 2. 验证密文长度(需包含认证标签) + if len(cipherBytes) <= authTagLengthByte { + return "", errors.New("密文长度不足,无法解析认证标签") + } + + // 3. 分离密文和认证标签(GCM模式中,认证标签通常附加在密文末尾) + ctext := cipherBytes[:len(cipherBytes)-authTagLengthByte] + authTag := cipherBytes[len(cipherBytes)-authTagLengthByte:] + + // 4. 初始化AES-GCM加密器 + block, err := aes.NewCipher(a.aesKey) + if err != nil { + return "", fmt.Errorf("创建AES加密器失败: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("创建GCM模式失败: %w", err) + } + + // 5. 构建附加数据(GCM的AAD) + additionalData := []byte(associatedData) + + // 6. 解密(GCM模式会自动验证认证标签) + plaintext, err := gcm.Open(nil, []byte(nonceStr), append(ctext, authTag...), additionalData) + if err != nil { + return "", fmt.Errorf("解密失败(可能是密钥错误或数据被篡改): %w", err) + } + + return string(plaintext), nil +} + +type Resource struct { + OriginalType string `json:"original_type"` + Algorithm string `json:"algorithm"` + Ciphertext string `json:"ciphertext"` // 如支付通知中的 encrypted_data + AssociatedData string `json:"associated_data"` // 微信支付回调的附加数据 通常为空字符串或回调相关信息 + Nonce string `json:"nonce"` // 12字节字符串 +} + +type WxNotifyBody struct { + Id string `json:"id"` + CreateTime string `json:"create_time"` + ResourceType string `json:"resource_type"` + EventType string `json:"event_type"` + Summary string `json:"summary"` + Resource Resource `json:"resource"` +} diff --git a/internal/pkg/wechat/utils/aes_test.go b/internal/pkg/wechat/utils/aes_test.go new file mode 100644 index 0000000..21f4d12 --- /dev/null +++ b/internal/pkg/wechat/utils/aes_test.go @@ -0,0 +1,22 @@ +package utils + +import ( + "testing" +) + +func TestAes(t *testing.T) { + + aesUtil, err := NewAesUtil("d9af70585b18ae206d981548c766563f") + if err != nil { + t.Errorf("NewAesUtil() error = %v", err) + return + } + + gotResponse, err := aesUtil.DecryptToString("coupon", "EGvd12Nf2Z1X", "+sUnGECvoHvsFE0ZnTT1ij/b8TKUxYS9jRh3gUSZbsO0WXEgXnwOvfOQjugnqx58lmYafa4BEoJ08c8La0HJpzZUthd6OEkcYlELcQOfqr72sikfFa5izbsNHh+hqd0fmUmpioyCre/BcrDneFqrOShrcLqUdb7FugI362Q1wgCgVJ1DqbGFvQuLHSHZ+UG0HY7MF7lt5r1w72tklHuvbxdfJZ2Vaj5muRUUpZIsZDZ76pqDlsTepxt5upWeVOBMLHjXCMEIJzcV0jF5tFgn7GTea/DORmRLEpY03uMiAyTYYaVbww1S0b96VuoTjVAAf0+oeiLtn+ZoWO1ZL5+Rbz1GTxivluoD+WVn/V5WUYNYnftiQ0LZcigxXIp+DYJOnuoLMDdWQCih4bElJGZnFuJIWpkS73Dn3HfbAx8lg0vq882PY/tk0bOXH6yrWGT9DO6ZHnPNABy4pb6xLRJtvmflSDhYc5Fa7qn0ySRAIK8e4C+z7aeKhzhWpNe2kGyjKKQqN92j9kMjsbfDqOSPRVB3FIxOvbzrJyR3erkN2WYRzTCZ6U7Qbgg/+tdWPdYNhAMrVbdmp2J1/daetCmmrd2HIIB6HqhsHYQMu0/NxQVPLGXuHzHY9fuNiYczST6KobLOFQ0fEO2dx6AFNImwHxHbdSHpchUqcE6sdjfoHNgSW7Jv4Fclm8GDqQmu4l6/ZhJrpn+I4ePijo7U/Ewh4KrCBHiw0JEttcZuBDqMM1Q9rmI4trcfekT2wy7HGgK9qetuOHMC5ENfO/U1gpa4wIU=") + if err != nil { + t.Errorf("DecryptToString() error = %v", err) + return + } + + t.Log(gotResponse) +} diff --git a/internal/pkg/wechat/utils/wxpay_utility.go b/internal/pkg/wechat/utils/wxpay_utility.go index f488688..d8718af 100644 --- a/internal/pkg/wechat/utils/wxpay_utility.go +++ b/internal/pkg/wechat/utils/wxpay_utility.go @@ -19,7 +19,6 @@ import ( "net/url" "os" "sort" - "strconv" "strings" "time" ) @@ -42,6 +41,8 @@ type MchConfig struct { wechatPayPublicKeyFilePath string privateKey *rsa.PrivateKey wechatPayPublicKey *rsa.PublicKey + + aesKey string } // MchId 商户号 @@ -76,6 +77,7 @@ func CreateMchConfig( privateKeyFilePath string, wechatPayPublicKeyId string, wechatPayPublicKeyFilePath string, + aesKey string, ) (*MchConfig, error) { mchConfig := &MchConfig{ mchId: mchId, @@ -83,6 +85,7 @@ func CreateMchConfig( privateKeyFilePath: privateKeyFilePath, wechatPayPublicKeyId: wechatPayPublicKeyId, wechatPayPublicKeyFilePath: wechatPayPublicKeyFilePath, + aesKey: aesKey, } privateKey, err := LoadPrivateKeyWithPath(mchConfig.privateKeyFilePath) if err != nil { @@ -293,13 +296,13 @@ func ValidateResponse( 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") - } + //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( @@ -572,6 +575,49 @@ func (srv *MchConfig) Verify(request *http.Request) (string, error) { return EncryptOAEPWithPublicKey(string(respBody), srv.wechatPayPublicKey) } +func (srv *MchConfig) GetNotifyBody(headers *http.Header, respBody []byte) (*WxNotifyBody, error) { + + if respBody == nil { + return nil, fmt.Errorf("request HttpBody is nil") + } + + err := ValidateResponse( + srv.WechatPayPublicKeyId(), + srv.WechatPayPublicKey(), + headers, + respBody, + ) + if err != nil { + return nil, err + } + + var wxNotifyBody WxNotifyBody + if err = json.Unmarshal(respBody, &wxNotifyBody); err != nil { + return nil, err + } + + return &wxNotifyBody, nil +} + +func (srv *MchConfig) DecodeBody(wxNotifyBody *WxNotifyBody) (string, error) { + + aesUtil, err := NewAesUtil(srv.aesKey) + if err != nil { + return "", err + } + + decryptedText, err := aesUtil.DecryptToString( + wxNotifyBody.Resource.AssociatedData, + wxNotifyBody.Resource.Nonce, + wxNotifyBody.Resource.Ciphertext, + ) + if err != nil { + return "", err + } + + return decryptedText, nil +} + // BuildSortedQueryString 函数接受一个 map,返回按照字段名排序后的 URL 键值对格式字符串 func BuildSortedQueryString(params map[string]any) string { // 创建一个字符串切片,用于保存所有的键名 diff --git a/internal/pkg/wechat/utils/wxpay_utility_test.go b/internal/pkg/wechat/utils/wxpay_utility_test.go new file mode 100644 index 0000000..fc464bd --- /dev/null +++ b/internal/pkg/wechat/utils/wxpay_utility_test.go @@ -0,0 +1,68 @@ +package utils + +import ( + "crypto/rsa" + "reflect" + "testing" +) + +func TestMchConfig_Request(t *testing.T) { + type fields struct { + mchId string + certificateSerialNo string + privateKeyFilePath string + wechatPayPublicKeyId string + wechatPayPublicKeyFilePath string + privateKey *rsa.PrivateKey + wechatPayPublicKey *rsa.PublicKey + } + type args struct { + host string + method string + path string + reqBody []byte + } + tests := []struct { + name string + fields fields + args args + wantResponse []byte + wantErr bool + }{ + { + name: "test", + fields: fields{ + mchId: "1652322442", + certificateSerialNo: "certificateSerialNo", + privateKeyFilePath: "privateKeyFilePath", + wechatPayPublicKeyId: "wechatPayPublicKeyId", + wechatPayPublicKeyFilePath: "wechatPayPublicKeyFilePath", + }, + args: args{ + host: "https://api.mch.weixin.qq.com", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv := &MchConfig{ + mchId: tt.fields.mchId, + certificateSerialNo: tt.fields.certificateSerialNo, + privateKeyFilePath: tt.fields.privateKeyFilePath, + wechatPayPublicKeyId: tt.fields.wechatPayPublicKeyId, + wechatPayPublicKeyFilePath: tt.fields.wechatPayPublicKeyFilePath, + privateKey: tt.fields.privateKey, + wechatPayPublicKey: tt.fields.wechatPayPublicKey, + } + + gotResponse, err := srv.Request(tt.args.host, tt.args.method, tt.args.path, tt.args.reqBody) + if (err != nil) != tt.wantErr { + t.Errorf("Request() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotResponse, tt.wantResponse) { + t.Errorf("Request() gotResponse = %v, want %v", gotResponse, tt.wantResponse) + } + }) + } +} diff --git a/internal/server/http.go b/internal/server/http.go index 0c52e1b..41abba8 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -11,7 +11,6 @@ import ( "github.com/go-kratos/kratos/v2/middleware/recovery" "github.com/go-kratos/kratos/v2/middleware/validate" "github.com/go-kratos/kratos/v2/transport/http" - "github.com/go-kratos/kratos/v2/transport/http/pprof" "github.com/gorilla/handlers" http2 "net/http" "time" @@ -27,25 +26,52 @@ func NewHTTPServer( log *log.Helper, accessLogger *log2.AccessLogger, cmb *service.CmbService, + tripartiteService *service.TripartiteService, + notifyService *service.NotifyService, ) *http.Server { //构建 server srv := buildHTTPServer(c, accessLogger, log) - srv.Handle("/voucher/debug/pprof/", pprof.NewHandler()) + + //srv.Handle("/voucher/debug/pprof/", pprof.NewHandler()) srv.Route("/voucher/").GET("ping", func(ctx http.Context) error { return ctx.String(http2.StatusOK, "pong") }) + // 启星(启星-蓝色兄弟立减金代配) /voucher/qixing/v1/notify + srv.Route("/voucher/").POST("qixing/v1/notify", tripartiteService.QiXingNotify) + // 通知url必须为公网可访问的URL,必须为HTTPS,不能携带参数 + // https://域名.cn/voucher/v1/notify/123456 123456为微信主体商户号 /voucher/v1/notify/1100040695 + // https://gateway.dev.cdlsxd.cn/voucher/v1/notify/123456 123456为微信主体商户号 测试环境 + // https://voucher.86698.cn/voucher/v1/notify/123456 123456为微信主体商户号 正式环境 + srv.Route("/voucher/").POST("/v1/notify/{mch_id}", notifyService.Notify) + srv.Route("/voucher/").POST("/v1/notifyMock", notifyService.NotifyMock) + + // ---脚本-- + // 订单通知重试 -- 不健全 + srv.Route("/voucher/").POST("orderNotifyRetry", cmb.OrderNotifyRetry) + // 重试订单通知,查询微信状态再通知下游招行 + srv.Route("/voucher/").POST("retryOrderNotice", cmb.RetryOrderNotice) + // 重试通知 srv.Route("/voucher/").POST("notifyRetry/{id}", cmb.NotifyRetry) - srv.Route("/voucher/").POST("queryOrder/{order_no}", cmb.QueryOrder) + // 查询订单状态及微信状态 + srv.Route("/voucher/").POST("queryOrder/{order_no}/{phone}/{is_notice}", cmb.QueryOrder) + srv.Route("/voucher/").GET("query-order/{order_no}/{phone}/{is_notice}", cmb.QueryOrder) + // 查询商品 srv.Route("/voucher/").POST("queryStock/{product_no}", cmb.QueryStock) + // 注册商品tag到刚哥那边 srv.Route("/voucher/").POST("registerTag/{id}", cmb.RegisterTag) + // 成功订单查询最终状态处理 srv.Route("/voucher/").POST("pushWechatQuery", cmb.PushWechatQuery) + // 订单查询最终状态处理 srv.Route("/voucher/").POST("timeSliceQueryPush", cmb.TimeSliceQueryPush) srv.Route("/voucher/").POST("pushWechatRetry/{batch_no}", cmb.PushWechatRetry) + // 商品预警通知 srv.Route("/voucher/").POST("warningBudget/{id}", cmb.WarningBudget) // 指定重复通知对应单子数据 srv.Route("/voucher/").POST("specifyNotification", cmb.SpecifyNotification) + // 订单使用通知下游 + srv.Route("/voucher/").POST("UsedNotifyPush", cmb.UsedNotifyPush) v1.RegisterCmbHTTPServer(srv, cmb) diff --git a/internal/server/rds_consume.go b/internal/server/rds_consume.go index 9a888f4..64e6ebd 100644 --- a/internal/server/rds_consume.go +++ b/internal/server/rds_consume.go @@ -26,6 +26,10 @@ func NewRdbConsumer( ) *RdbConsumer { manager := rdsmq.NewConsumerManager() + if cfTr := voucherService.GetRetryQueryNoticeConfig(); cfTr != nil { + manager.Add(cfTr) + } + if cf := voucherService.GetWechatQueryConfig(); cf != nil { manager.Add(cf) } @@ -34,14 +38,22 @@ func NewRdbConsumer( manager.Add(cf1) } - if cf2 := voucherService.GetWechatRetryConfig(); cf2 != nil { - manager.Add(cf2) - } + //if cf2 := voucherService.GetWechatRetryConfig(); cf2 != nil { + // manager.Add(cf2) + //} if cf3 := voucherService.GetRetryNotifyConfig(); cf3 != nil { manager.Add(cf3) } + if cf4 := voucherService.GetOrderNotifyRetryConfig(); cf4 != nil { + manager.Add(cf4) + } + + if cf5 := voucherService.GetUsedNotifyConfig(); cf5 != nil { + manager.Add(cf5) + } + return &RdbConsumer{hLog: hLog, conf: conf, manager: manager} } diff --git a/internal/server/wechat_consumer.go b/internal/server/wechat_consumer.go index 5a0ba87..c1241ef 100644 --- a/internal/server/wechat_consumer.go +++ b/internal/server/wechat_consumer.go @@ -150,10 +150,11 @@ func (w *WechatNotifyConsumer) processMessage(msg mq_http_sdk.ConsumeMessageEntr } }() + //log.Warnf("微信回调消费接收消息成功 messageId:%s, messageTag:%s, message:%s", msg.MessageId, msg.MessageTag, msg.MessageBody) + ctx := context.Background() if err := w.voucherService.WechatUseNotifyConsumer(ctx, msg.MessageTag, msg.MessageBody); err != nil { - //log.Errorf("微信回调消费接收消息成功,处理失败 messageId:%s, messageTag:%s, message:%s, err:%+v", msg.MessageId, msg.MessageTag, msg.MessageBody, err) - log.Errorf("微信回调消费接收消息成功,处理失败 messageId:%s, err:%+v", msg.MessageId, err) + log.Errorf("微信回调消费接收消息成功,处理失败 messageId:%s, messageTag:%s, message:%s, err:%+v", msg.MessageId, msg.MessageTag, msg.MessageBody, err) } } diff --git a/internal/service/notify.go b/internal/service/notify.go new file mode 100644 index 0000000..be8eb91 --- /dev/null +++ b/internal/service/notify.go @@ -0,0 +1,133 @@ +package service + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/transport/http" + "gorm.io/gorm" + "io" + http2 "net/http" + "voucher/internal/biz" + "voucher/internal/biz/bo" + "voucher/internal/pkg/helper" +) + +type NotifyService struct { + WechatBiz *biz.WechatBiz + VoucherBiz *biz.VoucherBiz +} + +func NewNotifyService(wechatBiz *biz.WechatBiz, VoucherBiz *biz.VoucherBiz) *NotifyService { + return &NotifyService{WechatBiz: wechatBiz, VoucherBiz: VoucherBiz} +} + +// Notify https://pay.weixin.qq.com/doc/v3/merchant/4012285250 +func (srv *NotifyService) Notify(ctx http.Context) error { + + mchId := ctx.Vars().Get("mch_id") + if mchId == "" { + log.Errorf("微信回调通知,mchId参数不能为空") + return ctx.JSON(http2.StatusNetworkAuthenticationRequired, map[string]string{ + "code": "FAIL", + "message": "微信回调通知,mchId参数不能为空", + }) + } + + headers := ctx.Request().Header + if headers == nil { + log.Errorf("微信回调通知[%s],headers参数不能为空", mchId) + return ctx.JSON(http2.StatusNetworkAuthenticationRequired, map[string]string{ + "code": "FAIL", + "message": "微信回调通知,headers参数不能为空", + }) + } + + bodyBytes, err := io.ReadAll(ctx.Request().Body) + if err != nil { + log.Errorf("微信回调通知[%s],读取响应体失败: %v", mchId, err) + return fmt.Errorf("读取响应体失败: %w", err) + } + + if len(bodyBytes) == 0 { + log.Errorf("微信回调通知[%s],响应体不能为空", mchId) + return ctx.JSON(http2.StatusNetworkAuthenticationRequired, map[string]string{ + "code": "FAIL", + "message": "微信回调通知,响应体不能为空", + }) + } + + bizData, err := srv.WechatBiz.CallBack(ctx, mchId, &headers, bodyBytes) + if err != nil { + headerJson, _ := json.Marshal(headers) + log.Errorf("微信回调通知[%s],callBack处理失败:%s\nheaders:%s\nbody:%s", mchId, err.Error(), headerJson, string(bodyBytes)) + return ctx.JSON(http2.StatusNetworkAuthenticationRequired, map[string]string{ + "code": "FAIL", + "message": err.Error(), + }) + } + + ip := helper.GetClientIP(ctx) + + if err = srv.VoucherBiz.WechatNotifyConsumer(ctx, ip, bizData); err != nil { + headerJson, _ := json.Marshal(headers) + log.Errorf("微信回调通知[%s],consumer处理失败:%s\nheaders:%s\nbody:%s\n解析数据:%+v", mchId, err.Error(), headerJson, string(bodyBytes), bizData) + + if errors.Is(err, gorm.ErrRecordNotFound) { + return ctx.JSON(http2.StatusOK, nil) + } + + return ctx.JSON(http2.StatusBadRequest, map[string]string{ + "code": "FAIL", + "message": err.Error(), + }) + } + + return ctx.JSON(http2.StatusOK, nil) +} + +// NotifyMock 模拟微信回调通知,用于测试 +func (srv *NotifyService) NotifyMock(ctx http.Context) error { + + bodyBytes, err := io.ReadAll(ctx.Request().Body) + if err != nil { + log.Errorf("微信回调通知,读取响应体失败: %v", err) + return fmt.Errorf("读取响应体失败: %w", err) + } + + if len(bodyBytes) == 0 { + log.Errorf("微信回调通知,响应体不能为空") + return ctx.JSON(http2.StatusOK, map[string]string{ + "code": "FAIL", + "message": "微信回调通知,响应体不能为空", + }) + } + + var bizData *bo.WechatVoucherNotifyBo + err = json.Unmarshal(bodyBytes, &bizData) + if err != nil { + log.Errorf("微信回调通知,解析响应体失败: %v", err) + return ctx.JSON(http2.StatusOK, map[string]string{ + "code": "FAIL", + "message": "微信回调通知,解析响应体失败", + }) + } + + ip := helper.GetClientIP(ctx) + + if err = srv.VoucherBiz.WechatNotifyConsumer(ctx, ip, bizData); err != nil { + log.Errorf("微信回调通知,consumer处理失败:%s\nbody:%s\n解析数据:%+v", err.Error(), string(bodyBytes), bizData) + + if errors.Is(err, gorm.ErrRecordNotFound) { + return ctx.JSON(http2.StatusOK, nil) + } + + return ctx.JSON(http2.StatusOK, map[string]string{ + "code": "FAIL", + "message": err.Error(), + }) + } + + return ctx.JSON(http2.StatusOK, nil) +} diff --git a/internal/service/order.go b/internal/service/order.go index 398076b..81d33a5 100644 --- a/internal/service/order.go +++ b/internal/service/order.go @@ -3,29 +3,28 @@ package service import ( "context" "encoding/json" - "github.com/go-kratos/kratos/v2/errors" - err2 "voucher/api/err" v1 "voucher/api/v1" "voucher/internal/biz/bo" + "voucher/internal/biz/businesserr" "voucher/internal/biz/vo" ) func (c *CmbService) Order(ctx context.Context, request *v1.CmbRequest) (*v1.CmbReply, error) { - orderNo, err := c.order(ctx, request) + order, product, err := c.order(ctx, request) if err != nil { - return c.OrderFail(ctx, err) + return c.OrderFail(ctx, order, product, err) } - return c.OrderSuccess(ctx, orderNo) + return c.OrderSuccess(ctx, order.OrderNo) } -func (c *CmbService) order(ctx context.Context, request *v1.CmbRequest) (string, error) { +func (c *CmbService) order(ctx context.Context, request *v1.CmbRequest) (*bo.OrderBo, *bo.ProductBo, error) { bizContent, err := c.CmbMixRepo.OrderVerify(ctx, request) if err != nil { - return "", err + return nil, nil, err } boReq := &bo.OrderCreateReqBo{ @@ -39,12 +38,12 @@ func (c *CmbService) order(ctx context.Context, request *v1.CmbRequest) (string, NotifyUrl: c.bc.Cmb.NotifyUrl, } - orderNo, err := c.VoucherBiz.CmbOrder(ctx, boReq) + order, product, err := c.VoucherBiz.CmbOrder(ctx, boReq) if err != nil { - return "", err + return nil, nil, err } - return orderNo, nil + return order, product, nil } func (c *CmbService) OrderSuccess(ctx context.Context, orderNo string) (*v1.CmbReply, error) { @@ -60,19 +59,45 @@ func (c *CmbService) OrderSuccess(ctx context.Context, orderNo string) (*v1.CmbR return c.GetResponse(ctx, replyBizContent) } -func (c *CmbService) OrderFail(ctx context.Context, err error) (*v1.CmbReply, error) { +func (c *CmbService) OrderFail(ctx context.Context, order *bo.OrderBo, product *bo.ProductBo, err error) (*v1.CmbReply, error) { - se := errors.FromError(err) + bizReply := &v1.CmbOrderReply{} - if len(se.Reason) == 0 { - se.Reason = err2.CmbErr_CMB_UNKNOWN.String() - } + if e, ok := err.(*businesserr.BusinessErr); ok { - bizReply := &v1.CmbOrderReply{ - RespCode: vo.CmbResponseStatusFail.GetValue(), - RespMsg: se.Message, - CodeNo: "", - ThirdErrCode: se.Reason, + isMultiErr := false + + if product != nil && product.ActivityId != "" { + isMultiErr = true + } else if order != nil && order.ActivityId != "" { + isMultiErr = true + } + + if isMultiErr { + cmbAPIError := e.HandleMULTIThirdErrCode() + bizReply = &v1.CmbOrderReply{ + RespCode: vo.CmbResponseStatusFail.GetValue(), + RespMsg: cmbAPIError.Description, + CodeNo: "", + ThirdErrCode: string(cmbAPIError.ThirdErrCode), + } + } else { + cmbAPIError := e.HandleThirdErrCode() + bizReply = &v1.CmbOrderReply{ + RespCode: vo.CmbResponseStatusFail.GetValue(), + RespMsg: cmbAPIError.Description, + CodeNo: "", + ThirdErrCode: string(cmbAPIError.ThirdErrCode), + } + } + + } else { + bizReply = &v1.CmbOrderReply{ + RespCode: vo.CmbResponseStatusFail.GetValue(), + RespMsg: err.Error(), + CodeNo: "", + ThirdErrCode: string(businesserr.ThirdErrCodeDefault), + } } replyBizContent, _ := json.Marshal(bizReply) diff --git a/internal/service/order_norify_retry.go b/internal/service/order_norify_retry.go new file mode 100644 index 0000000..89f6c2e --- /dev/null +++ b/internal/service/order_norify_retry.go @@ -0,0 +1,45 @@ +package service + +import ( + "context" + "fmt" + "github.com/go-kratos/kratos/v2/log" + "voucher/internal/pkg/rdsmq" +) + +func (s *VoucherService) GetOrderNotifyRetryConfig() *rdsmq.ConsumeConfig { + + queue := s.bc.RdsMQ.GetOrderNotifyRetry() + if queue == nil { + return nil + } + + if !queue.GetIsOpen() { + log.Warn(fmt.Sprintf("[%s]RdsMQ is not open", queue.Name)) + return nil + } + + return &rdsmq.ConsumeConfig{ + Rdb: s.rdb.Rdb, + QueueName: queue.Name, + NumWorkers: queue.NumWorkers, + WaitTime: queue.GetWaitTime().AsDuration(), + RetryNum: queue.RetryNum, + Fn: s.HandleOrderNotifyRetry, + Logger: s.logHelper, + } +} + +func (s *VoucherService) HandleOrderNotifyRetry(ctx context.Context, msg string) error { + + if msg == "" { + s.logHelper.Errorf("RdsMQ keySend error: msg is empty") + return nil + } + + if err := s.VoucherBiz.OrderNotifyRetry(ctx, msg); err != nil { + s.logHelper.Error(err) + } + + return nil +} diff --git a/internal/service/provider_set.go b/internal/service/provider_set.go index 984a7a0..220ae5c 100644 --- a/internal/service/provider_set.go +++ b/internal/service/provider_set.go @@ -8,4 +8,6 @@ import ( var ProviderSetService = wire.NewSet( NewVoucherService, NewCmbService, + NewTripartiteService, + NewNotifyService, ) diff --git a/internal/service/qixing.go b/internal/service/qixing.go new file mode 100644 index 0000000..ba36742 --- /dev/null +++ b/internal/service/qixing.go @@ -0,0 +1,102 @@ +package service + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/transport/http" + "io" + http2 "net/http" + "voucher/internal/biz" + "voucher/internal/biz/bo" + "voucher/internal/conf" + "voucher/internal/pkg/helper" + "voucher/internal/pkg/supplier/qixing" +) + +type TripartiteService struct { + bc *conf.Bootstrap + multiBiz *biz.MultiBiz +} + +func NewTripartiteService(bc *conf.Bootstrap, multiBiz *biz.MultiBiz) *TripartiteService { + return &TripartiteService{bc: bc, multiBiz: multiBiz} +} + +// QiXingNotify +// 重试规则 +// 1.重试间隔:每次推送失败后,间隔5分钟重新推送; +// 2.最大重试次数:累计推送5 次(首次 + 4 次重试; +// 3.停止条件:返回成功响应 或 达到 4 次重试上限,停止推送。 +func (srv *TripartiteService) QiXingNotify(ctx http.Context) error { + + ip := helper.GetClientIP(ctx) + if ip == "" { + return fmt.Errorf("获取请求 IP 失败") + } + + bodyBytes, err := io.ReadAll(ctx.Request().Body) + if err != nil { + return srv.ResponseErr(ctx, fmt.Sprintf("read body error: %v", err)) + } + + var req *qixing.QiXingRequestBo + if err = json.Unmarshal(bodyBytes, &req); err != nil { + log.Errorf("qixing notify bodyBytes err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes)) + return srv.ResponseErr(ctx, fmt.Sprintf("json unmarshal bodyBytes error: %v", err)) + } + + if err = req.Validate(); err != nil { + log.Errorf("qixing notify req validate err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes)) + return srv.ResponseErr(ctx, fmt.Sprintf("validate req error: %v", err)) + } + + // 加密校验串,(生成规则:MD5(content+公钥)) + if srv.bc.Tripartite.QiXing.AppKey == "" { + return srv.ResponseErr(ctx, "qixing appKey is empty") + } + + sign := helper.Md5(req.Content + srv.bc.Tripartite.QiXing.AppKey) + if sign != req.Ciphertext { + log.Errorf("qixing notify sign err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes)) + return srv.ResponseErr(ctx, "verify sign error") + } + + wxNotifyDataStr, err := base64.StdEncoding.DecodeString(req.Content) + if err != nil { + log.Errorf("qixing notify wxNotifyDataStr err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes)) + return srv.ResponseErr(ctx, fmt.Sprintf("base64 decode req content error: %v", err)) + } + + var wxNotifyData *bo.WechatVoucherNotifyBo + if err = json.Unmarshal(wxNotifyDataStr, &wxNotifyData); err != nil { + log.Errorf("qixing notify wxNotifyData err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes)) + return srv.ResponseErr(ctx, fmt.Sprintf("json unmarshal wxNotifyData error: %v", err)) + } + + if err = wxNotifyData.Validate(); err != nil { + log.Errorf("qixing notify wxNotifyData validate err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes)) + return srv.ResponseErr(ctx, fmt.Sprintf("validate wxNotifyData error: %v", err)) + } + + err = srv.multiBiz.Notify(ctx, ip, "qixing_"+wxNotifyData.PlainText.StockCreatorMchid, wxNotifyData) + if err != nil { + log.Errorf("qixing notify run err ip:%s,error:%v,body:%s", ip, err, string(bodyBytes)) + return srv.ResponseErr(ctx, err.Error()) + } + + return srv.ResponseOK(ctx) +} + +func (this *TripartiteService) ResponseOK(ctx http.Context) error { + return ctx.JSON(http2.StatusOK, &qixing.QiXingResponse{ + Msg: "SUCCESS", + }) +} + +func (this *TripartiteService) ResponseErr(ctx http.Context, msg string) error { + return ctx.JSON(http2.StatusOK, &qixing.QiXingResponse{ + Msg: msg, + }) +} diff --git a/internal/service/script.go b/internal/service/script.go index 2a11c8a..cd7d2b9 100644 --- a/internal/service/script.go +++ b/internal/service/script.go @@ -11,6 +11,48 @@ import ( "voucher/internal/biz/do" ) +func (this *CmbService) RetryOrderNotice(ctx http.Context) error { + + bodyBytes, err := io.ReadAll(ctx.Request().Body) + if err != nil { + return err + } + + if bodyBytes == nil { + return fmt.Errorf("bodyBytes is empty") + } + + var req *do.RetryQueryNotice + if err = json.Unmarshal(bodyBytes, &req); err != nil { + return err + } + + return this.VoucherBiz.PushRetryOrderNotice(ctx, bodyBytes) +} + +func (this *CmbService) OrderNotifyRetry(ctx http.Context) error { + + bodyBytes, err := io.ReadAll(ctx.Request().Body) + if err != nil { + return err + } + + var req *do.OrderNotifyRetry + if err = json.Unmarshal(bodyBytes, &req); err != nil { + return err + } + + if req == nil { + return fmt.Errorf("req is empty") + } + + if req.StartTime == "" || req.EndTime == "" { + return fmt.Errorf("start_time or end_time is empty") + } + + return this.VoucherBiz.PushOrderNotifyRetry(ctx, req) +} + func (this *CmbService) NotifyRetry(ctx http.Context) error { id := ctx.Vars().Get("id") if id == "" { @@ -25,21 +67,40 @@ func (this *CmbService) NotifyRetry(ctx http.Context) error { return this.VoucherBiz.PushNotifyRetryDelayMQ(ctx, 1, orderNotifyId) } +var ds = map[string]bool{ + "13474987525": true, + "15221117226": true, + "18666173766": true, +} + func (this *CmbService) QueryOrder(ctx http.Context) error { + phone := ctx.Vars().Get("phone") + if phone == "" { + return fmt.Errorf("phone is empty") + } + + _, ok := ds[phone] + if !ok { + return fmt.Errorf("无权限~") + } + orderNo := ctx.Vars().Get("order_no") if orderNo == "" { return fmt.Errorf("orderNo is empty") } - str, err := this.VoucherBiz.QueryOrder(ctx, orderNo) + isNotice := ctx.Vars().Get("is_notice") + + str, err := this.VoucherBiz.QueryOrder(ctx, orderNo, isNotice) if err != nil { return err } - return ctx.JSON(http2.StatusOK, map[string]interface{}{ - "data": str, - }) + ctx.Response().Header().Add("Content-Type", "text/plain; charset=utf-8") + ctx.Response().WriteHeader(http2.StatusOK) + _, err = ctx.Response().Write([]byte(str)) + return err } func (this *CmbService) QueryStock(ctx http.Context) error { @@ -183,3 +244,31 @@ func (this *CmbService) SpecifyNotification(ctx http.Context) error { return ctx.String(http2.StatusOK, string(bodyBytes)) } + +func (this *CmbService) UsedNotifyPush(ctx http.Context) error { + + bodyBytes, err := io.ReadAll(ctx.Request().Body) + if err != nil { + return err + } + + var req *do.WechatUsedQuery + if err = json.Unmarshal(bodyBytes, &req); err != nil { + return err + } + + if req == nil { + return fmt.Errorf("req is empty") + } + + if req.StartTime == "" && req.EndTime == "" { + return fmt.Errorf("start time or end time is empty") + } + + err = this.VoucherBiz.UsedNotifyPush(ctx, req) + if err != nil { + return err + } + + return ctx.String(http2.StatusOK, string(bodyBytes)) +} diff --git a/internal/service/used_notify.go b/internal/service/used_notify.go new file mode 100644 index 0000000..860a707 --- /dev/null +++ b/internal/service/used_notify.go @@ -0,0 +1,45 @@ +package service + +import ( + "context" + "fmt" + "github.com/go-kratos/kratos/v2/log" + "voucher/internal/pkg/rdsmq" +) + +func (s *VoucherService) GetUsedNotifyConfig() *rdsmq.ConsumeConfig { + + queue := s.bc.RdsMQ.GetUsedNotify() + if queue == nil { + return nil + } + + if !queue.GetIsOpen() { + log.Warn(fmt.Sprintf("[%s]RdsMQ is not open", queue.Name)) + return nil + } + + return &rdsmq.ConsumeConfig{ + Rdb: s.rdb.Rdb, + QueueName: queue.Name, + NumWorkers: queue.NumWorkers, + WaitTime: queue.GetWaitTime().AsDuration(), + RetryNum: queue.RetryNum, + Fn: s.HandleUsedNotify, + Logger: s.logHelper, + } +} + +func (s *VoucherService) HandleUsedNotify(ctx context.Context, msg string) error { + + if msg == "" { + s.logHelper.Errorf("RdsMQ used notify error: msg is empty") + return nil + } + + if err := s.VoucherBiz.UsedNotify(ctx, msg); err != nil { + s.logHelper.Error(err) + } + + return nil +} diff --git a/internal/service/voucher.go b/internal/service/voucher.go index b13c6d4..07b3211 100644 --- a/internal/service/voucher.go +++ b/internal/service/voucher.go @@ -133,5 +133,5 @@ func (v *VoucherService) WechatUseNotifyConsumer(ctx context.Context, tag, msg s return err } - return v.VoucherBiz.WechatNotifyConsumer(ctx, tag, req) + return v.VoucherBiz.WechatNotifyConsumer(ctx, "127.0.0.1", req) } diff --git a/internal/service/wechat_query.go b/internal/service/wechat_query.go index 6e44485..dac4b56 100644 --- a/internal/service/wechat_query.go +++ b/internal/service/wechat_query.go @@ -80,3 +80,30 @@ func (s *VoucherService) WechatTimeSliceQueryHandle(ctx context.Context, msg str return nil } + +func (s *VoucherService) GetRetryQueryNoticeConfig() *rdsmq.ConsumeConfig { + + return &rdsmq.ConsumeConfig{ + Rdb: s.rdb.Rdb, + QueueName: "retryQueryNotice", + NumWorkers: 1, + WaitTime: 30, + RetryNum: 1, + Fn: s.RetryQueryNotice, + Logger: s.logHelper, + } +} + +func (s *VoucherService) RetryQueryNotice(ctx context.Context, msg string) error { + + if msg == "" { + s.logHelper.Errorf("wechat TimeSlice query error: batchNo is empty") + return nil + } + + if err := s.timeSliceQuery.RetryQueryNotice(ctx, msg); err != nil { + s.logHelper.Errorf("retry query notice msg:%s error: %v", msg, err) + } + + return nil +} diff --git a/test/bank_multi_activity.go b/test/bank_multi_activity.go index 54f1352..1c137f4 100644 --- a/test/bank_multi_activity.go +++ b/test/bank_multi_activity.go @@ -36,6 +36,7 @@ func marketing() *marketing2.Marketing { fmt.Sprintf("%s/%s", filePath, "wechat_private_key.pem"), // 商户API证书私钥文件路径,本地文件路径 wechatPayPublicKeyId, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径 + "", ) if err != nil { panic(err) @@ -56,13 +57,9 @@ func marketingFJxw() *marketing2.Marketing { //openssl x509 -in xxx.pem -noout -serial mchId := "1652465541" - wechatPayPublicKeyId := "PUB_KEY_ID_0117109533612025031800326400002563" + wechatPayPublicKeyId := " PUB_KEY_ID_0116524655412025070900181741001803" certificateSerialNo := "1E3F2CE013203BA9C3DEFC5782FCD3329C3DAC1C" - //mchId := "1652465541" - //wechatPayPublicKeyId := "" - //certificateSerialNo := "1E3F2CE013203BA9C3DEFC5782FCD3329C3DAC1C" - filePath := fmt.Sprintf("%s/cert/wechat/%s", parentDir, mchId) c, err := utils.CreateMchConfig( @@ -71,6 +68,38 @@ func marketingFJxw() *marketing2.Marketing { fmt.Sprintf("%s/%s", filePath, "wechat_private_key.pem"), // 商户API证书私钥文件路径,本地文件路径 wechatPayPublicKeyId, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径 + "", + ) + if err != nil { + panic(err) + } + + return &marketing2.Marketing{MchConfig: c} +} + +func marketingFJLF() *marketing2.Marketing { + + dir, err := os.Getwd() + if err != nil { + panic(err) + } + + parentDir := filepath.Dir(dir) + + mchId := "1100040695" + wechatPayPublicKeyId := "PUB_KEY_ID_0111000406952026032500382251001000" + certificateSerialNo := "46712853869DB0EDAA9B4DF97DADEECD4CCDC85B" + + filePath := fmt.Sprintf("%s/cert/wechat/%s", parentDir, mchId) + fmt.Printf("filePath: %s\n", filePath) + + c, err := utils.CreateMchConfig( + mchId, // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 + certificateSerialNo, // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 + fmt.Sprintf("%s/%s", filePath, "wechat_private_key.pem"), // 商户API证书私钥文件路径,本地文件路径 + wechatPayPublicKeyId, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 + fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径 + "d9af70585b18ae206d981548c766563f", ) if err != nil { panic(err) @@ -81,27 +110,13 @@ func marketingFJxw() *marketing2.Marketing { func MarketingSend() { - //openId := "oSNb4ftgnWC22Z0cWTjsQebdr2Yk" - //appId := "wx619991cc795028f5" + openId := "oSNb4ftgnWC22Z0cWTjsQebdr2Yk" + appId := "wx619991cc795028f5" - //195516196845312409613 - openId := "ocuH-0Nymo4sJLRNabIBbg9H2XCo" - appId := "wx5d3e839568f24b2b" - //respBody={"coupon_id":"116076813524"} - - //openId := "ocZ-njugTd_fgCJMHTG8PukPAVm4" - //appId := "wxd9137161bc8f9ca9" - //respBody={"coupon_id":"117888124542"} - - //195516196845312409613 - //1958100775326560252 - //1958100775326560251 - //19581007753265602565 - //19581007753265602564 request := &marketing2.SendReq{ - ActivityId: utils.String("11941580000000005"), - StockId: utils.String("20847510"), - OutRequestNo: utils.String("195516196845312409613"), + ActivityId: utils.String("11941580000000012"), + StockId: utils.String("20964154"), + OutRequestNo: utils.String("196605139814767820814"), Appid: utils.String(appId), StockCreatorMchId: utils.String("1652465541"), } @@ -128,17 +143,7 @@ func MarketingSend() { fmt.Printf("请求成功: %s\n", *response.CouponId) } -func MarketingQuery() { - - appId := "wx619991cc795028f5" - openId := "oSNb4ftgnWC22Z0cWTjsQebdr2Yk" - couponId := "113831004454" - - response, err := marketing().Query(appId, openId, couponId) - if err != nil { - fmt.Print(err) - return - } - - fmt.Printf("请求成功: %+v\n", response) +func MarketingQuery(appId, openId, couponId string) (response *marketing2.SendResp, err error) { + return marketingFJxw().Query(appId, openId, couponId) + //return marketingFJLF().Query(appId, openId, couponId) } diff --git a/test/bank_multi_activity_test.go b/test/bank_multi_activity_test.go index 907c1bf..5e3fb47 100644 --- a/test/bank_multi_activity_test.go +++ b/test/bank_multi_activity_test.go @@ -1,8 +1,22 @@ package test -import "testing" +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "testing" + "time" + "voucher/internal/biz/bo" + "voucher/internal/biz/businesserr" + "voucher/internal/pkg/helper" + "voucher/internal/pkg/supplier/qixing" + "voucher/internal/pkg/wechat/utils" +) -func TestMarketingSend(t *testing.T) { +func Test_MarketingSend(t *testing.T) { tests := []struct { name string }{ @@ -16,3 +30,125 @@ func TestMarketingSend(t *testing.T) { }) } } + +func Test_MarketingQuery(t *testing.T) { + + appId := "wx619991cc795028f5" + openId := "oSNb4fixFZ4uRvcP6F25_FySgUE0" + couponId := "147079366189" + + tests := []struct { + name string + appId, openId, couponId string + }{ + { + name: "查询绑定多笔立减活动的代金券详情", // 查询的商户非创建方商户 查询商户要为创建方商户 + appId: appId, + openId: openId, + couponId: couponId, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := MarketingQuery(tt.appId, tt.openId, tt.couponId) + if err != nil { + + var e *utils.ApiException + if errors.As(err, &e) { + // 格式:{"code":"INVALID_REQUEST","message":"对应单号已超出重试期;请查单确认后决定是否换单请求"} + var beer *businesserr.BusinessErr + if err = json.Unmarshal(e.Body(), &beer); err != nil { + t.Errorf("微信错误返回body解析报错,body:%s,err:%s", string(e.Body()), err.Error()) + } + t.Logf("MarketingQuery error,body:%s,err:%s", string(e.Body()), e.ErrCode) + } else { + t.Errorf("MarketingQuery() error = %v", err) + } + + return + } + t.Logf("MarketingQuery() = %v", resp) + }) + } + + // 合单订单就是没有核销回调通知的 + // {"code":"APPID_MCHID_NOT_MATCH","message":"商户号与AppID不匹配"} + // APPID_MCHID_NOT_MATCH 商户号与AppID不匹配 调用接口的商户号需与接口传入的AppID有绑定关系 + + //openid不是自己的appid下的喔,这也能查询到吗” + //不行的,需要是在自己appid下的才能查到 +} + +func Test_QixingNotifyDataDecodeString(t *testing.T) { + + content := `eyJzdW1tYXJ5Ijoi5Luj6YeR5Yi45qC46ZSA6YCa55+lIiwiYXNzb2NpYXRlZF9kYXRhIjoiY291cG9uIiwiZXZlbnRfdHlwZSI6IkNPVVBPTi5VU0UiLCJjcmVhdGVfdGltZSI6IjIwMjUtMTItMjRUMTk6NDg6NDYrMDg6MDAiLCJwbGFpbl90ZXh0Ijp7InN0b2NrX2NyZWF0b3JfbWNoaWQiOiIxNzE1MzQ5NTc4Iiwic3RvY2tfaWQiOiIyMTQzODg1MSIsImNvdXBvbl9pZCI6IjE0NDQ2MTIyOTg5MCIsInNpbmdsZWl0ZW1fZGlzY291bnRfb2ZmIjpudWxsLCJkaXNjb3VudF90byI6bnVsbCwiY291cG9uX25hbWUiOiLpk7booYzljaHlpJrnrJTnq4vlh48iLCJzdGF0dXMiOiJVU0VEIiwiZGVzY3JpcHRpb24iOiIiLCJjcmVhdGVfdGltZSI6IjIwMjUtMTItMThUMTU6NDQ6NTIrMDg6MDAiLCJjb3Vwb25fdHlwZSI6Ik5PUk1BTCIsIm5vX2Nhc2giOmZhbHNlLCJhdmFpbGFibGVfYmVnaW5fdGltZSI6IjIwMjUtMTItMThUMDA6MDA6MDArMDg6MDAiLCJhdmFpbGFibGVfZW5kX3RpbWUiOiIyMDI2LTAxLTE2VDIzOjU5OjU5KzA4OjAwIiwic2luZ2xlaXRlbSI6ZmFsc2UsIm5vcm1hbF9jb3Vwb25faW5mb3JtYXRpb24iOnsiY291cG9uX2Ftb3VudCI6MzAwLCJ0cmFuc2FjdGlvbl9taW5pbXVtIjoxODAwfSwiY29uc3VtZV9pbmZvcm1hdGlvbiI6eyJjb25zdW1lX3RpbWUiOiIyMDI1LTEyLTI0VDE5OjQ4OjQ2KzA4OjAwIiwiY29uc3VtZV9tY2hpZCI6IjEyNzQ5Mzg2MDEiLCJ0cmFuc2FjdGlvbl9pZCI6IjQyMDAwMDI5MTcyMDI1MTIyNDgzODQ2MTg0MzgiLCJjb25zdW1lX2Ftb3VudCI6MzAwLCJnb29kc19kZXRhaWwiOm51bGx9fSwicmVzb3VyY2VfdHlwZSI6ImVuY3J5cHQtcmVzb3VyY2UiLCJvcmlnaW5hbF90eXBlIjoiY291cG9uIiwiaWQiOiI4NzkxZTMyZS0zYjFiLTViNDktYTNiNi1mYzY0YTc5NTVlZjEifQ==` + + b, err := base64.StdEncoding.DecodeString(content) + if err != nil { + t.Errorf("base64.StdEncoding.DecodeString() error = %v", err) + return + } + + t.Log(string(b)) +} + +func Test_QixingNotifyData(t *testing.T) { + + //wxBody := `{"id":"4ab2699d-e91d-5460-9810-25fd6d4c69a5","create_time":"2025-12-08T17:54:24+08:00","resource_type":"encrypt-resource","event_type":"COUPON.USE","summary":"代金券核销通知","original_type":"coupon","associated_data":"coupon","plain_text":{"stock_creator_mchid":"1652465541","stock_id":"21386484","coupon_id":"142388354994","coupon_name":"银行卡多笔立减","description":"","status":"SENDED","create_time":"2025-12-08T17:50:48+08:00","coupon_type":"NORMAL","no_cash":false,"singleitem":false,"business_type":"","consume_information":{"consume_time":"2025-12-08T17:54:24+08:00","consume_mchid":"1274938601","transaction_id":"4200002996202512083063051834","consume_amount":16}}}` + wxBody := `{"id":"4ab2699d-e91d-5460-9810-25fd6d4c69a6","create_time":"2025-12-08T17:54:24+08:00","resource_type":"encrypt-resource","event_type":"COUPON.USE","summary":"代金券核销通知","original_type":"coupon","associated_data":"coupon","plain_text":{"stock_creator_mchid":"1652465541","stock_id":"21386484","coupon_id":"118758145502","coupon_name":"银行卡多笔立减","description":"","status":"USED","create_time":"2025-12-08T17:50:48+08:00","coupon_type":"NORMAL","no_cash":false,"singleitem":false,"business_type":"","consume_information":{"consume_time":"2025-12-08T17:54:24+08:00","consume_mchid":"1274938601","transaction_id":"4200002996202512083063051834","consume_amount":16}}}` + t.Log(len(wxBody)) + + content := base64.StdEncoding.EncodeToString([]byte(wxBody)) + + ciphertext := helper.Md5(content + "DrY1zLkOH01q0sN66vrmkdpbWsyb41ho") + + req := qixing.QiXingRequestBo{ + Content: content, + Timestamp: time.Now().UnixMilli(), + Ciphertext: ciphertext, + } + + b, _ := json.Marshal(req) + + t.Log(string(b)) + // {"content":"base64(微信通知json对象数据)","timestamp":1765447477945,"ciphertext":"md5(base64(微信通知json对象数据)+key)"} + //t.Logf(`{"content":"%s","timestamp":122345677890,"ciphertext":"%s"}`, content, ciphertext) +} + +func Test_MarketingNotify(t *testing.T) { + + headerBytes := []byte(`{"Accept":["*/*"],"Cache-Control":["no-cache"],"Connection":["close"],"Content-Length":["1137"],"Content-Type":["application/json"],"Pragma":["no-cache"],"User-Agent":["Mozilla/4.0"],"Wechatpay-Nonce":["kHzkvdHwssdU0CRpFfgCpzdxtdzQGsIS"],"Wechatpay-Serial":["PUB_KEY_ID_0111000406952026032500382251001000"],"Wechatpay-Signature":["jt/2zYvqTlvOHAb9Lb1bfbLUDnqy59dc1JF87AiHVtagZAxzWNP5Jgsrr/jv9C3UVv+MHvbTxuaDQjJAfXx4CT7ihYUNEF6rQL/ilToSMuZpw23/pPjyAzXvBWBsj3AY3rxfa4OkaviRnG6vRA5HKnaHHG5wmDdrcwOoKBiLJ6cax8OYu9GV8Opr0uSzWj7ZxPoSxXy65MxEaampVXJcLnCm1iVp2mHZH6jafBxyjhDGIZ6uOJD0LdCUCJMfbDKvlthO7CrfLRsdosVrVmnL3lJU2ti5rjngmzAxHFi+J4JUsbTvkWnBEXZXaXZ4vNi4gkGnOGPJHs4ch+s0MV/ZFg=="],"Wechatpay-Signature-Type":["WECHATPAY2-SHA256-RSA2048"],"Wechatpay-Timestamp":["1774605950"],"X-Forwarded-For":["121.51.58.172, 172.17.0.1"]}`) + bodyBytes := []byte(`{"id":"4a5e22d9-9018-5bfc-94ba-3c9957a62355","create_time":"2026-03-27T18:05:50+08:00","resource_type":"encrypt-resource","event_type":"COUPON.USE","summary":"代金券核销通知","resource":{"original_type":"coupon","algorithm":"AEAD_AES_256_GCM","ciphertext":"aTS2kVk7l/lzfIEUMUVfk5+6ouRSakspFTP/nR4sDPp0nvrj2VtEQBXkYqTgDDxs35V9CrJI90X31Mho2+adZm1ScPPH0HL6iUaUcypzgvHAhSaJBhugz8slqoQ3zaynBKa/HpyU6Jnfd3OTbhcrS2i27cBdWawd2UQ5HBgAemV8/k/gf+LPC04fJNFfrrttTxqtBDpXr3H/ob5pV825C57SJJhpXxgZtnX/e9avpWZlPIXhOSA3FSwYZtoW1BYoXrsLID6fGcP5IJuc8EE40Z7si95tQbQcM1eqH19OZbRAmXitV+sY1pOlo0Zehnc5vyH5RunVYa2lwdsirSGU1EI0PlDYwHoNxUEtSpKZ4MP8IReNdkjKlwpBQXtgZMGqMNdDU+/Db/ZUr/R8xaF7RoHfiiTHEHQaEK9xbCZji+F8YDiU0K8I8getKsyHiZwHZrU67p/6ql6zVzcWBjaDyL71tcl1k1ppkyOFdg9g0WqcJD56xa9qMhXaOyQoIU4hJsMMTvBWxyRelE4+o1Z79nUVUMswvg/hPZ4QEeIF8C+ezzk+/PhggYhZE4g4WnzqKRh/WASXRja0UsucBVIs9hViV+aDuQVEjxmy29S3UTrwL0Kr0+5hUz7q87gJUaAvehF6pIPtvliJvR0tnpdCrLHuIsc/LiSxoX2WSkGGZet58swdJ7wHYzdfNFKbhRS2IBj4JzI6wc+zRkGmLCijTRFSadZGnNZf5SPez1iEqr0fecCDjN6NUMv/YETfjeEqUxRvNVjMK9vCzEm+bdVJNNL+3i5RiJA9Bng1JtmTMsHatgn8dg==","associated_data":"coupon","nonce":"L3TIxaSQ12pD"}}`) + + httpHeaders := make(http.Header) + if err := json.Unmarshal(headerBytes, &httpHeaders); err != nil { + fmt.Printf("headers Unmarshal err: %+v\n", err) + return + } + + body, bizContent, err := marketingFJLF().Notify(context.Background(), &httpHeaders, bodyBytes) + if err != nil { + t.Errorf("notify err: %+v\n", err) + return + } + + var plainText bo.PlainText + if err = json.Unmarshal([]byte(bizContent), &plainText); err != nil { + t.Errorf("Unmarshal err: %+v\n", err) + return + } + + notifyBo := bo.WechatVoucherNotifyBo{ + ID: body.Id, + CreateTime: body.CreateTime, + ResourceType: body.ResourceType, + EventType: body.EventType, + Summary: body.Summary, + OriginalType: body.Resource.OriginalType, + AssociatedData: body.Resource.AssociatedData, + PlainText: plainText, + } + + fmt.Printf("bizContent: %s\n", bizContent) + fmt.Printf("notifyBo: %+v\n", notifyBo) +} diff --git a/test/cmb_test.go b/test/cmb_test.go new file mode 100644 index 0000000..0181fdc --- /dev/null +++ b/test/cmb_test.go @@ -0,0 +1,166 @@ +package test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + "time" + v1 "voucher/api/v1" + "voucher/internal/biz/vo" + "voucher/internal/pkg/cmb" + "voucher/internal/pkg/helper" + "voucher/internal/pkg/request" +) + +func Test_CMBRequest(t *testing.T) { + + bizParma := &v1.CmbNotifyRequest{ + Ticket: "190662195271022592015", // 优惠券券码,codeNo + Status: "0", // 更新后串码状态,0:可使用,1:已使用 + TransDate: "", // 验码日期,格式yyyy-mm-dd hh:mm:ss.sss + OrgNo: "LANSEXIONGDI", // 固定值 + Ext: "", // 扩展字段 + Attach: "bFUWzuzvJfBshobjcQPFTvpqcH1AvGqHJtiV44mdsWEQKCPXgydk8ft/b227S3TM", // 扩展字段 + TransactionId: "2000000000001422730", // 招行唯一流水号 + } + + bizParmaJsonBytes, err := json.Marshal(bizParma) + if err != nil { + t.Error(err) + return + } + + encryptBody, err := cmb.EncryptBody(&cmb.Encrypts{SoaPubKey: "0416445bc16cbf42e47002ad9fe7c7af67d902b48be1eb69b98f6a006b0918630e1127f5f2fff83b2ecb30fc7fd72c34c33f37c7c355dffde3589f66800f0036ca", JsonParam: string(bizParmaJsonBytes)}) + if err != nil { + t.Error(err) + return + } + + req := &v1.CmbRequest{ + Mid: "d6fdd78b6fd13a808818286b9cad9687", + Aid: "5efaa21263b94f669a1c90ed0279df20", + Date: time.Now().Format("20060102150405"), + Random: string(cmb.RandomBytes(16)), + KeyAlias: "CO_PUB_KEY_SM2", + CmbKeyAlias: "SM2_CMBLIFE", + EncryptBody: encryptBody, + Sign: "", + } + + str := fmt.Sprintf("%s?%s", vo.CmbNotifyFuncName, cmb.SortStructStr(req)) + + sign, err := cmb.SignBody(str, "8d39ff3d2559258c163f4510f082727f51531e1953ab203d5ab1ea4a6d94fd73") + if err != nil { + t.Error(err) + return + } + + req.Sign = sign + + kvRows := helper.SortStructFieldsByKey(req) + + uv := url.Values{} + + for _, kv := range kvRows { + uv.Set(kv.Key, fmt.Sprintf("%v", kv.Value)) + } + + h := http.Header{ + "Content-Type": []string{"application/x-www-form-urlencoded"}, + } + + uri := "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatus.json" + r := uri + "?" + uv.Encode() + + respHeader, bodyBytes, err := request.Post(context.Background(), r, nil, request.WithHeaders(h), request.WithTimeout(time.Second*10)) + if err != nil { + t.Error(err) + return + } + + t.Logf("请求地址:%s", uri) + t.Logf("业务参数:%s", string(bizParmaJsonBytes)) + t.Logf("请求响应体:%s", string(bodyBytes)) + t.Logf("请求响应头:%+v", respHeader) +} + +func Test_CmMultiRequest(t *testing.T) { + + bizParma := &v1.CmbMultiNotifyRequest{ + TransactionId: "2000000000001636670", // 招行唯一流水号 + ActivityId: "11941580000000008", + CouponId: "118770338167", + AcquiredDate: "2025-08-25 14:56:54.123", + Status: "1", // 更新后串码状态,0:可使用,1:已使用 + CouponStatus: "1", // String|M 整张券总状态0:可使用,1:已使用 + TransDate: "2025-08-28 12:09:41.123", // 验码日期,格式yyyy-mm-dd hh:mm:ss.sss + TransAmount: "10", + OrderId: "4200002772202508280813272296", + Ticket: "195987259358664294429", // 优惠券券码,codeNo + OrgNo: "LANSEXIONGDI", // 固定值 + Attach: "bFUWzuzvJfBshobjcQPFTvpqcH1AvGqHJtiV44mdsWEQKCPXgydk8ft/b227S3TM", // 扩展字段 + Ext: "", // 扩展字段 + } + + bizParmaJsonBytes, err := json.Marshal(bizParma) + if err != nil { + t.Error(err) + return + } + + encryptBody, err := cmb.EncryptBody(&cmb.Encrypts{SoaPubKey: "0416445bc16cbf42e47002ad9fe7c7af67d902b48be1eb69b98f6a006b0918630e1127f5f2fff83b2ecb30fc7fd72c34c33f37c7c355dffde3589f66800f0036ca", JsonParam: string(bizParmaJsonBytes)}) + if err != nil { + t.Error(err) + return + } + + req := &v1.CmbRequest{ + Mid: "d6fdd78b6fd13a808818286b9cad9687", + Aid: "5efaa21263b94f669a1c90ed0279df20", + Date: time.Now().Format("20060102150405"), + Random: string(cmb.RandomBytes(16)), + KeyAlias: "CO_PUB_KEY_SM2", + CmbKeyAlias: "SM2_CMBLIFE", + EncryptBody: encryptBody, + Sign: "", + } + + str := fmt.Sprintf("%s?%s", vo.CmbNotifyFuncNameUpdateCodeStatusForMulti, cmb.SortStructStr(req)) + + sign, err := cmb.SignBody(str, "8d39ff3d2559258c163f4510f082727f51531e1953ab203d5ab1ea4a6d94fd73") + if err != nil { + t.Error(err) + return + } + + req.Sign = sign + + kvRows := helper.SortStructFieldsByKey(req) + + uv := url.Values{} + + for _, kv := range kvRows { + uv.Set(kv.Key, fmt.Sprintf("%v", kv.Value)) + } + + h := http.Header{ + "Content-Type": []string{"application/x-www-form-urlencoded"}, + } + + uri := "https://sandbox.cdcc.cmbchina.com/AccessGateway/transIn/updateCodeStatusForMulti.json" + r := uri + "?" + uv.Encode() + + respHeader, bodyBytes, err := request.Post(context.Background(), r, nil, request.WithHeaders(h), request.WithTimeout(time.Second*10)) + if err != nil { + t.Error(err) + return + } + + t.Logf("请求地址:%s", uri) + t.Logf("业务参数:%s", string(bizParmaJsonBytes)) + t.Logf("请求响应体:%s", string(bodyBytes)) + t.Logf("请求响应头:%+v", respHeader) +} diff --git a/test/coupon.go b/test/coupon.go index eddbb02..0c42d4c 100644 --- a/test/coupon.go +++ b/test/coupon.go @@ -10,7 +10,9 @@ import ( "os" "path/filepath" "time" - "voucher/internal/biz/do" + v1 "voucher/api/v1" + "voucher/internal/biz/businesserr" + "voucher/internal/biz/vo" "voucher/internal/conf" "voucher/internal/data" ) @@ -24,14 +26,34 @@ var bc = &conf.Bootstrap{ }, } -func SendCoupon() { +// bcFJXW Callback{NotifyUrl:https://nsall.86698.cn/wechatPay/coupon_notify/fjxingwang, Mchid:1652465541} +var bcFJXW = &conf.Bootstrap{ + Wechat: &conf.Wechat{ + MchID: "1652465541", // notifyUrl https://nsall.86698.cn/wechatPay/coupon_notify/fjxingwang + MchCertificateSerialNumber: "1E3F2CE013203BA9C3DEFC5782FCD3329C3DAC1C", + WechatPayPublicKeyID: "PUB_KEY_ID_0116524655412025070900181741001803", + Name: "福建兴旺", + }, +} + +// bcFJLF Callback{NotifyUrl:https://gateway.dev.cdlsxd.cn/voucher/v1/notify/1100040695, Mchid:1100040695} +// /voucher/v1/notify/1100040695 +var bcFJLF = &conf.Bootstrap{ + Wechat: &conf.Wechat{ + MchID: "1100040695", + MchCertificateSerialNumber: "46712853869DB0EDAA9B4DF97DADEECD4CCDC85B", + WechatPayPublicKeyID: "PUB_KEY_ID_0111000406952026032500382251001000", + Name: "福建峦峰", + }, +} + +func SendCoupon() error { ctx := context.Background() dir, err := os.Getwd() if err != nil { - fmt.Printf("os.Getwd() error = %v", err) - return + return fmt.Errorf("os.Getwd() error = %v", err) } parentDir := filepath.Dir(dir) @@ -43,17 +65,16 @@ func SendCoupon() { } client, err := data.GetClient(ctx, server) if err != nil { - fmt.Println(err) - return + return fmt.Errorf("data.GetClient(ctx, server) error = %v", err) } req := cashcoupons.SendCouponRequest{ - OutRequestNo: core.String("LQ1948534036766040066"), + OutRequestNo: core.String("LQ2011991700944699394"), // {CouponId:129623470711} // 微信为发券方商户分配的公众账号ID,接口传入的所有appid应该为公众号的appid(在mp.weixin.qq.com申请的),不能为APP的appid(在open.weixin.qq.com申请的)。 Appid: core.String("wxd27e255810842ba8"), - Openid: core.String("o3dEt5cA8jt3Kz5wNzAO6-3YQHsE"), - StockId: core.String("20391098"), - StockCreatorMchid: core.String("1652465541"), + Openid: core.String("o3dEt5b_1lFtKc-aAT3tiYjJIGwk"), + StockId: core.String("21502886"), + StockCreatorMchid: core.String("16524655411111"), } fmt.Printf("\nreq:%+v", req) @@ -62,15 +83,26 @@ func SendCoupon() { resp, result, err := svc.SendCoupon(ctx, req) if err != nil { - fmt.Println(err) - return + + bodyBytes, err := io.ReadAll(result.Response.Body) + if err != nil { + return fmt.Errorf("读取微信错误返回body报错:%s", err.Error()) + } + fmt.Printf("\nbodyBytes:%s\n", string(bodyBytes)) + + var beer *businesserr.BusinessErr + if err = json.Unmarshal(bodyBytes, &beer); err != nil { + return fmt.Errorf("微信错误返回body解析报错,body:%s,err:%s", string(bodyBytes), err.Error()) + } + + return beer } fmt.Printf("\nresp:%+v\n result:%+v", resp, result) - return + return nil } -func QueryCoupon() { +func QueryCoupon(appId, openId, couponId string) { ctx := context.Background() @@ -93,32 +125,6 @@ func QueryCoupon() { return } - //req := cashcoupons.QueryCouponRequest{ - // //CouponId: core.String("101270193400"), - // CouponId: core.String("101270193400"), - // Appid: core.String("wx619991cc795028f5"), - // Openid: core.String("oSNb4fmScVLmXILaolXVdBpJmYbQ"), - //} - - //req := cashcoupons.QueryCouponRequest{ - // CouponId: core.String("101274529925"), - // Appid: core.String("wx619991cc795028f5"), - // Openid: core.String("oSNb4fpnLNdkFPk-O43o6f2hxxRo"), - //} - - //appId := "wx619991cc795028f5" - //openId := "oSNb4ftgnWC22Z0cWTjsQebdr2Yk" - //couponId := "113831004454" - //req := cashcoupons.QueryCouponRequest{ - // CouponId: core.String(couponId), - // Appid: core.String(appId), - // Openid: core.String(openId), - //} - - appId := "wx5d3e839568f24b2b" - openId := "ocuH-0Nymo4sJLRNabIBbg9H2XCo" - couponId := "116076813524" - req := cashcoupons.QueryCouponRequest{ CouponId: core.String(couponId), Appid: core.String(appId), @@ -170,8 +176,8 @@ func QueryProduct() { } req := cashcoupons.QueryStockRequest{ - StockId: core.String("20847510"), - StockCreatorMchid: core.String("1652465541"), + StockId: core.String("21928191"), + StockCreatorMchid: core.String("1100040695"), } svc := cashcoupons.StockApiService{Client: client} @@ -181,8 +187,8 @@ func QueryProduct() { return } - j, _ := json.Marshal(resp) - fmt.Printf("\nresp:%s\n", string(j)) + originData, _ := json.Marshal(resp) + fmt.Printf("\nOriginData:%s\n", string(originData)) availableStock := *resp.StockUseRule.MaxCoupons - *resp.DistributedCoupons couponAmount := *resp.StockUseRule.FixedNormalCoupon.CouponAmount / 100 @@ -196,47 +202,54 @@ func QueryProduct() { fmt.Printf("\n剩余库存:%d", availableStock) fmt.Printf("\n剩余预算:%d", availableStock*couponAmount) - fmt.Printf("\nWxResp:%+v", WxResp(resp)) + WxResp(resp) return } -func WxResp(wxResp *cashcoupons.Stock) (reps *do.WxResp) { +func WxResp(wxResp *cashcoupons.Stock) { - availableStock := *wxResp.StockUseRule.MaxCoupons - *wxResp.DistributedCoupons - couponAmount := *wxResp.StockUseRule.FixedNormalCoupon.CouponAmount / 100 - - remainingBudget := availableStock * couponAmount - - stockUsageRate := float64(*wxResp.DistributedCoupons) / float64(*wxResp.StockUseRule.MaxCoupons) * 100 - - req := &do.WxResp{ - Amount: couponAmount, - AllBudget: *wxResp.StockUseRule.MaxAmount / 100, - AllStock: *wxResp.StockUseRule.MaxCoupons, - UsedStock: *wxResp.DistributedCoupons, - UsedBudget: *wxResp.DistributedCoupons * couponAmount, - AvailableStock: availableStock, - AvailableBudget: remainingBudget, - StockUsageRate: stockUsageRate, + xx := &v1.CmbQueryProductReply{ + RespCode: vo.CmbResponseStatusSuccess.GetValue(), + RespMsg: "成功", + ActivityName: *wxResp.StockName, + ActivityId: "CMB" + *wxResp.StockId, + Amount: "", + MinAmount: "", + AvailableType: "", + AvailableDays: "", // 动态有效期天数 + StartTime: "", + EndTime: "", + AvailableStock: "", + Detail: *wxResp.Description, } inputFormat := time.RFC3339 if wxResp.AvailableBeginTime != nil { + availableBeginTime, _ := time.Parse(inputFormat, *wxResp.AvailableBeginTime) - req.StartTime = &availableBeginTime + xx.StartTime = availableBeginTime.Format("2006-01-02 15:04:05.000") + xx.SaleStartTime = xx.StartTime } if wxResp.AvailableEndTime != nil { availableEndTime, _ := time.Parse(inputFormat, *wxResp.AvailableEndTime) - req.EndTime = &availableEndTime + xx.EndTime = availableEndTime.Format("2006-01-02 15:04:05.000") + xx.SaleEndTime = xx.EndTime } - return req + xx.Amount = fmt.Sprintf("%d", *wxResp.StockUseRule.FixedNormalCoupon.CouponAmount) + xx.MinAmount = fmt.Sprintf("%d", *wxResp.StockUseRule.FixedNormalCoupon.TransactionMinimum) + + availableStock := *wxResp.StockUseRule.MaxCoupons - *wxResp.DistributedCoupons + xx.AvailableStock = fmt.Sprintf("%d", availableStock) + + cmbData, _ := json.Marshal(xx) + fmt.Printf("\nCMB[%s]", string(cmbData)) } -func QueryCallback() { +func QueryCallback(bc *conf.Bootstrap) { ctx := context.Background() @@ -262,7 +275,47 @@ func QueryCallback() { svc := cashcoupons.CallBackUrlApiService{Client: client} response, _, err := svc.QueryCallback(ctx, cashcoupons.QueryCallbackRequest{ - Mchid: core.String("1652465541"), + Mchid: core.String(bc.Wechat.MchID), + }) + if err != nil { + fmt.Println(err) + return + } + + fmt.Printf("\nresp:%+v\n", response) + return +} + +func SetCallback(bc *conf.Bootstrap) { + + ctx := context.Background() + + dir, err := os.Getwd() + if err != nil { + fmt.Printf("os.Getwd() error = %v", err) + return + } + parentDir := filepath.Dir(dir) + + server := data.Server{ + MchID: bc.Wechat.MchID, + MchCertificateSerialNumber: bc.Wechat.MchCertificateSerialNumber, + WechatPayPublicKeyID: bc.Wechat.WechatPayPublicKeyID, + Dir: parentDir, + } + client, err := data.GetClient(ctx, server) + if err != nil { + fmt.Println(err) + return + } + + svc := cashcoupons.CallBackUrlApiService{Client: client} + + response, _, err := svc.SetCallback(ctx, cashcoupons.SetCallbackRequest{ + Mchid: core.String(bc.Wechat.MchID), + //NotifyUrl: core.String("https://gateway.dev.cdlsxd.cn/voucher/v1/notify/" + bc.Wechat.MchID), + NotifyUrl: core.String("https://voucher.86698.cn/voucher/v1/notify/" + bc.Wechat.MchID), + Switch: core.Bool(true), }) if err != nil { fmt.Println(err) diff --git a/test/coupon_test.go b/test/coupon_test.go index 61daa6f..e6b38d9 100644 --- a/test/coupon_test.go +++ b/test/coupon_test.go @@ -2,6 +2,7 @@ package test import ( "testing" + "voucher/internal/biz/businesserr" ) func Test_SendCoupon(t *testing.T) { @@ -14,22 +15,38 @@ func Test_SendCoupon(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - SendCoupon() + if err := SendCoupon(); err != nil { + if ee, ok := err.(*businesserr.BusinessErr); ok { + t.Errorf("errorcode:%s,errmsg:%s", ee.Code, ee.Message) + } + } }) } } func Test_QueryCoupon(t *testing.T) { tests := []struct { - name string + name string + appId string + openId string + couponId string }{ { - name: "查询券订单信息", + name: "查询券订单信息", + appId: "wx619991cc795028f5", + openId: "oSNb4fnRWr7HdgbDOO8TD66LXofE", + couponId: "147089832782", + }, + { + name: "查询券订单信息", + appId: "wx619991cc795028f5", + openId: "oSNb4foo87dBNx1D9KTH-bB-G6YA", + couponId: "147094824239", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - QueryCoupon() + QueryCoupon(tt.appId, tt.openId, tt.couponId) }) } } @@ -60,7 +77,23 @@ func Test_QueryCallback(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - QueryCallback() + QueryCallback(bcFJLF) + }) + } +} + +func Test_SetCallback(t *testing.T) { + tests := []struct { + name string + }{ + { + name: "设置券核销通知地址", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + SetCallback(bcFJLF) }) } }