From 9e35fe6e5e69c0335713391dc553c08c1f5339e8 Mon Sep 17 00:00:00 2001 From: yangduo Date: Mon, 6 Jan 2025 11:07:54 +0800 Subject: [PATCH] wx notify --- .../api/v1/mainservice/mainservice.go | 190 ++++++++++++++++++ server/payserver/constant/constant.go | 9 + server/payserver/model/inapp_order.go | 2 +- server/payserver/mtb/mtb.auto_gen.go | 10 + server/payserver/proto/mt.proto | 1 + .../router/mainservice/mainservice.go | 2 + server/payserver/service/wxpay.go | 130 +++++++++++- server/payserver/service/wxpaybase.go | 135 +++++++++++++ 8 files changed, 468 insertions(+), 11 deletions(-) create mode 100644 server/payserver/service/wxpaybase.go diff --git a/server/payserver/api/v1/mainservice/mainservice.go b/server/payserver/api/v1/mainservice/mainservice.go index 5061c58..347ea21 100644 --- a/server/payserver/api/v1/mainservice/mainservice.go +++ b/server/payserver/api/v1/mainservice/mainservice.go @@ -1,10 +1,17 @@ package mainservice import ( + "crypto/sha1" + "encoding/json" "f5" + "io" "main/constant" "main/service" + "payserver/model" + "payserver/mt" "q5" + "sort" + "strings" "github.com/gin-gonic/gin" ) @@ -36,3 +43,186 @@ func (this *MainServiceApi) RefreshToken(c *gin.Context) { service.Wxpay.GetAccessTokenList(&rspObj.Data) c.JSON(200, rspObj) } + +func (this *MainServiceApi) WxTNotify(c *gin.Context) { + f5.GetSysLog().Debug("wx test notify:%s", c.Request.URL.RawQuery) + + signature := c.Query("signature") + timestamp := c.Query("timestamp") + nonce := c.Query("nonce") + echostr := c.Query("echostr") + strs := []string{constant.WX_NOTIFY_TOKEN, timestamp, nonce} + sort.Strings(strs) + sb := strings.Builder{} + sb.WriteString(strs[0]) + sb.WriteString(strs[1]) + sb.WriteString(strs[2]) + m := sha1.New() + io.WriteString(m, sb.String()) + sign := string(m.Sum(nil)) + + f5.GetSysLog().Debug("wx sign:%s, %s", sign, signature) + + if sign != signature { + c.String(200, "wrong") + return + } + c.String(200, echostr) +} + +func (this *MainServiceApi) WxNotifyPurchase(c *gin.Context) { + f5.GetSysLog().Debug("wx notify purchase:%s", c.Request.URL.RawQuery) + + signature := c.Query("signature") + timestamp := c.Query("timestamp") + nonce := c.Query("nonce") + + strs := []string{constant.WX_NOTIFY_TOKEN, timestamp, nonce} + sort.Strings(strs) + sb := strings.Builder{} + sb.WriteString(strs[0]) + sb.WriteString(strs[1]) + sb.WriteString(strs[2]) + m := sha1.New() + io.WriteString(m, sb.String()) + sign := string(m.Sum(nil)) + + f5.GetSysLog().Debug("wx sign:%s, %s", sign, signature) + if sign != signature { + return + } + + rspObj := struct { + ErrorCode int32 `json:"ErrCode"` + ErrMsg string `json:"ErrMsg"` + }{ + ErrorCode: 99999, + ErrMsg: "internal error", + } + + msg_signature := c.Query("msg_signature") + if msg_signature != "" { + postObj := struct { + Encrypt string `json:"Encrypt"` + ToUserName string `json:"ToUserName"` + }{} + + if err := c.ShouldBindJSON(&postObj); err != nil { + rspObj.ErrorCode = 401 + rspObj.ErrMsg = "post data error" + c.JSON(200, rspObj) + return + } + + smsg, appid := service.Wxpay.DecryptMsg(msg_signature, timestamp, nonce, postObj.Encrypt) + if smsg == nil || appid == nil || len(smsg) == 0 || len(appid) == 0 { + rspObj.ErrorCode = 402 + rspObj.ErrMsg = "decrypt data error" + c.JSON(200, rspObj) + return + } + + f5.GetSysLog().Debug("wx decrypt msg:%s", smsg) + + wxnotifyobj := service.WxPurchaseNotify{} + if json.Unmarshal(smsg, &wxnotifyobj) != nil { + rspObj.ErrorCode = 403 + rspObj.ErrMsg = "unmarshal data error" + c.JSON(200, rspObj) + return + } + + gameid := int64(0) + appkey := "" + notifyurl := "" + mt.Table.Wxconfig.Traverse(func(w *mt.Wxconfig) bool { + if w.GetAppid() == string(appid) { + gameid = w.GetGameid() + appkey = w.GetAppkey() + notifyurl = w.GetNotifyurl() + return false + } + return true + }) + + if appkey == "" { + f5.GetSysLog().Error("wx app config error:%s", appid) + c.JSON(200, rspObj) + return + } + + oristr := wxnotifyobj.Event + "&" + wxnotifyobj.MiniGame.Payload + sig := service.Wxpay.GenSHA256Signature(oristr, appkey) + if sig != wxnotifyobj.MiniGame.PayEventSig { + f5.GetSysLog().Error("pay event sig error:%s, %s, %s", appid, sig, wxnotifyobj.MiniGame.PayEventSig) + c.JSON(200, rspObj) + return + } + + payloadobj := new(service.WxPayload) + if json.Unmarshal([]byte(wxnotifyobj.MiniGame.Payload), &payloadobj) != nil { + c.JSON(200, rspObj) + return + } + + envpass := true + if f5.IsOnlineEnv() { + if payloadobj.Env != 0 { + f5.GetSysLog().Error("notify test info to prod url") + envpass = false + } + } else { + if payloadobj.Env != 1 { + f5.GetSysLog().Error("notify prod info to test url") + envpass = false + } + } + + if !envpass { + c.JSON(200, rspObj) + return + } + + orderModel := new(model.InAppOrder) + if err, found := orderModel.FindByOrderId(payloadobj.OutTradeNo); err != nil { + c.JSON(200, rspObj) + return + } else if !found { + c.JSON(200, rspObj) + return + } + + if orderModel.Status > 1 { + rspObj.ErrorCode = 0 + rspObj.ErrMsg = "Success" + c.JSON(200, rspObj) + return + } + + rediskey := "ls:accountid:" + orderModel.AccountId + str, err := service.Redis.Get(constant.LOGIN_REDIS, rediskey) + if err != nil { + c.JSON(200, rspObj) + return + } + + data := map[string]interface{}{} + if json.Unmarshal([]byte(str), &data) != nil { + c.JSON(200, rspObj) + return + } + + openid := q5.SafeToString(data["openid"]) + if openid != payloadobj.OpenId { + c.JSON(200, rspObj) + return + } + + orderModel.GameId = int32(gameid) + f5.GetSysLog().Debug("notify url:%s, %s", appid, notifyurl) + + } + + c.JSON(200, rspObj) + +} diff --git a/server/payserver/constant/constant.go b/server/payserver/constant/constant.go index 1e3d700..170e05c 100644 --- a/server/payserver/constant/constant.go +++ b/server/payserver/constant/constant.go @@ -56,3 +56,12 @@ const ( const ( GLOBAL_SALT = "f3a6a9a5-217a-4079-ab99-b5d69b8212be" ) + +const ( + WX_NOTIFY_TOKEN = "dV93f4FwSGMwkYcvsRHD8egdW5egPMhF" //必须32位 + WX_NOTIFY_ENCODING_AES_KEY = "H60uFIXjyd431hLVhlsKyus3U28RVIzWncey424DqpY" + WX_AESKEY_SIZE = 32 + WX_ENCODING_KEY_SIZE = 43 + WX_RANDENCRYPT_STRLEN = 16 + WX_KMSG_LEN = 4 +) diff --git a/server/payserver/model/inapp_order.go b/server/payserver/model/inapp_order.go index f92fa55..ab4a0d1 100644 --- a/server/payserver/model/inapp_order.go +++ b/server/payserver/model/inapp_order.go @@ -23,7 +23,7 @@ type InAppOrder struct { TryCount int32 `gorm:"column:try_count;comment:补单次数"` Price int32 `gorm:"column:price;comment:price"` IP string `gorm:"column:ipv4;comment:ipv4地址"` - Status int32 `gorm:"column:status;comment:0: 新添加订单 1:已经完成订单"` + Status int32 `gorm:"column:status;comment:0: 新添加订单 1:已支付 2:已发货"` ConfirmTime int32 `gorm:"column:confirmtime;comment:GameServer订单确认时间"` CreateTime int32 `gorm:"column:createtime;<-:create"` ModifyTime int32 `gorm:"column:modifytime"` diff --git a/server/payserver/mtb/mtb.auto_gen.go b/server/payserver/mtb/mtb.auto_gen.go index 4efe013..9453a71 100644 --- a/server/payserver/mtb/mtb.auto_gen.go +++ b/server/payserver/mtb/mtb.auto_gen.go @@ -119,6 +119,7 @@ type Wxconfig struct { appsecret string zoneid string offerid string + notifyurl string _flags1_ uint64 _flags2_ uint64 @@ -568,6 +569,14 @@ func (this *Wxconfig) HasOfferid() bool { return (this._flags1_ & (uint64(1) << 6)) > 0 } +func (this *Wxconfig) GetNotifyurl() string { + return this.notifyurl +} + +func (this *Wxconfig) HasNotifyurl() bool { + return (this._flags1_ & (uint64(1) << 7)) > 0 +} + func (this *LoginRedis) GetHost() string { return this.host } @@ -702,6 +711,7 @@ func (this *Wxconfig) LoadFromKv(kv map[string]interface{}) { f5.ReadMetaTableField(&this.appsecret, "appsecret", &this._flags1_, 4, kv) f5.ReadMetaTableField(&this.zoneid, "zoneid", &this._flags1_, 5, kv) f5.ReadMetaTableField(&this.offerid, "offerid", &this._flags1_, 6, kv) + f5.ReadMetaTableField(&this.notifyurl, "notifyurl", &this._flags1_, 7, kv) } func (this *LoginRedis) LoadFromKv(kv map[string]interface{}) { diff --git a/server/payserver/proto/mt.proto b/server/payserver/proto/mt.proto index c95bfee..9eefec2 100644 --- a/server/payserver/proto/mt.proto +++ b/server/payserver/proto/mt.proto @@ -98,6 +98,7 @@ message Wxconfig optional string appsecret = 4; optional string zoneid = 5; optional string offerid = 6; + optional string notifyurl = 7; } message LoginRedis diff --git a/server/payserver/router/mainservice/mainservice.go b/server/payserver/router/mainservice/mainservice.go index 6ca91ae..fbf847a 100644 --- a/server/payserver/router/mainservice/mainservice.go +++ b/server/payserver/router/mainservice/mainservice.go @@ -11,4 +11,6 @@ func (this *MainServiceRouter) InitRouter() { api := v1.ApiGroupApp.MainServiceApiGroup f5.GetApp().GetGinEngine().GET("/api/service/refresh", api.RefreshToken) + f5.GetApp().GetGinEngine().GET("/wx/tnotify", + api.WxTNotify) } diff --git a/server/payserver/service/wxpay.go b/server/payserver/service/wxpay.go index 9341d76..321fa28 100644 --- a/server/payserver/service/wxpay.go +++ b/server/payserver/service/wxpay.go @@ -1,16 +1,12 @@ package service import ( - "crypto/hmac" - "crypto/sha256" - "encoding/hex" "encoding/json" "errors" "f5" "main/constant" "main/mt" "q5" - "strings" "time" ) @@ -58,6 +54,50 @@ type WxPayRsp struct { UsedGift int64 `json:"used_present_amount"` } +type WxQueryOrderRsp struct { + ErrCode int32 `json:"errcode"` + ErrMsg string `json:"errmsg"` + ProductId string `json:"product_id"` //道具id + PayState int32 `json:"pay_state"` //1:未支付;2:已支付 + DeliverState int32 `json:"deliver_state"` //1:未发货;2:已发货 + PayTime int64 `json:"pay_finish_time"` //支付完成时间 + TrandNo string `json:"out_trade_no"` //用户订单号 + MchOrderNo string `json:"mch_order_no"` //微信支付商户单 + Trans string `json:"transaction_id"` //微信支付订单号 +} + +type WxPurchaseNotify struct { + ToUserName string `json:"ToUserName"` //小游戏原始ID + FromUserName string `json:"FromUserName"` //该事件消息的openid,道具发货场景固定为微信官方的openid + CreateTime int64 `json:"CreateTime"` //消息发送时间 + MsgType string `json:"MsgType"` //消息类型,道具发货场景固定为:event + Event string `json:"Event"` //事件类型 商城道具场景固定为:minigame_h5_goods_deliver_notify 道具直购(游戏内)场景固定为:minigame_game_pay_goods_deliver_notify + MiniGame struct { + Payload string `json:"Payload"` // 携带的具体内容,格式为json,具体内容如下表格Payload(因为这里需要对消息内容统一签名,所以统一把消息内容设计成json格式) + PayEventSig string `json:"PayEventSig"` //见https://docs.qq.com/doc/DVVZZdHFsYkttYmxl(PayEventSig) + } `json:"MiniGame"` //道具直购发货参数 +} + +type WxPayload struct { + OpenId string `json:"OpenId"` //接收道具的玩家openid + Env int32 `json:"Env"` //环境配置 0:现网环境(也叫正式环境) 1:沙箱环境 + OutTradeNo string `json:"OutTradeNo"` // 订单号 + GoodsInfo struct { + ProductId string `json:"ProductId"` //游戏道具id标识 + Quantity int64 `json:"Quantity"` //购买道具数量 + ZoneId string `json:"ZoneId"` //分区 + OrigPrice int64 `json:"OrigPrice"` //物品原始价格 (单位:分) + ActualPrice int64 `json:"ActualPrice"` //物品实际支付价格(单位:分) + Attach string `json:"Attach"` //透传数据 + OrderSource int64 `json:"OrderSource"` // 1 游戏内 2 商城下单 3 商城测试下单 + } `json:"GoodsInfo"` //发货道具 + WeChatPayInfo struct { + MchOrderNo string `json:"MchOrderNo"` // 微信支付商户单号 + TransactionId string `json:"TransactionId"` // 交易单号(微信支付订单号) + } `json:"WeChatPayInfo"` //微信支付信息(仅微信支付渠道) + +} + func (wp *wxpay) init() { wp.gamesGoods = q5.ConcurrentMap[int64, map[int64]int64]{} wp.accessTokens = q5.ConcurrentMap[int64, TokenInfo]{} @@ -333,6 +373,82 @@ func (wp *wxpay) QueryPay(openid string, gameid int64, userip string, sessionkey return wxerrcode } +func (wp *wxpay) QueryPurchase(openid string, gameid int64, userip string, sessionkey string, amount int32, tradeno string) (wxerrcode int32) { + cfg := mt.Table.Wxconfig.GetById(gameid) + postbody := struct { + OpenId string `json:"openid"` + Ts int64 `json:"ts"` + Env int32 `json:"env"` + TradeNo string `json:"out_trade_no"` + BizId int32 `json:"biz_id"` + OfferId string `json:"offer_id"` + }{ + OpenId: openid, + Ts: f5.GetApp().GetRealSeconds(), + BizId: 2, //1代币 2道具直购 + TradeNo: tradeno, + OfferId: cfg.GetOfferid(), + } + + if !f5.IsOnlineEnv() { + postbody.Env = 1 + } + + poststr := q5.EncodeJson(postbody) + + queryuri := "/wxa/game/queryorderinfo" + query := WxQuery{ + AccessToken: wp.getAccessToken(gameid), + Signature: wp.GenSHA256Signature(poststr, sessionkey), + SigMethod: "hmac_sha256", + PaySig: wp.GenSHA256Signature(queryuri+"&"+poststr, cfg.GetAppkey()), + } + + params := map[string]string{} + data, _ := json.Marshal(query) + json.Unmarshal(data, ¶ms) + + sendRequest := false + urls := mt.Table.Config.GetWxUrl() + for _, wxhost := range urls { + url := "https://" + wxhost + queryuri + f5.GetHttpCliMgr().SendGoStylePost( + url, + params, + "Content-Type: application/json", + poststr, + func(rsp f5.HttpCliResponse) { + if rsp.GetErr() != nil { + return + } + + sendRequest = true + rspJson := WxQueryOrderRsp{} + f5.GetSysLog().Debug("wx query order rsp:%s", rsp.GetRawData()) + err := q5.DecodeJson(rsp.GetRawData(), &rspJson) + if err != nil { + return + } + + wxerrcode = rspJson.ErrCode + switch rspJson.ErrCode { + case constant.WX_ERRCODE_OK: + case constant.WX_ERRCODE_BUSY: + sendRequest = false + default: + f5.GetSysLog().Info("err msg:%s", rspJson.ErrMsg) + wp.checkErrorCode(rspJson.ErrCode) + } + + }) + if sendRequest { + break + } + } + + return wxerrcode +} + func (wp *wxpay) AddExpireInfo(accountid string, sessiontime int64) { info := expireSession{ SessionTime: sessiontime, @@ -356,12 +472,6 @@ func (wp *wxpay) CheckExpireCache(accountid string, expire int64) bool { return true } -func (wp *wxpay) GenSHA256Signature(str string, key string) string { - mac := hmac.New(sha256.New, []byte(key)) - _, _ = mac.Write([]byte(str)) - return strings.ToLower(hex.EncodeToString(mac.Sum(nil))) -} - func (wp *wxpay) GetAccessTokenList(data *[]TokenInfo) { wp.accessTokens.Range(func(key int64, value TokenInfo) bool { *data = append(*data, value) diff --git a/server/payserver/service/wxpaybase.go b/server/payserver/service/wxpaybase.go new file mode 100644 index 0000000..ecfd5c0 --- /dev/null +++ b/server/payserver/service/wxpaybase.go @@ -0,0 +1,135 @@ +package service + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "main/constant" + "sort" + "strings" +) + +func (wp *wxpay) GenSHA256Signature(str string, key string) string { + mac := hmac.New(sha256.New, []byte(key)) + _, _ = mac.Write([]byte(str)) + return strings.ToLower(hex.EncodeToString(mac.Sum(nil))) +} + +func (wp *wxpay) DecryptMsg(sMsgSignature string, sTimeStamp string, sNonce string, sEncryptMsg string) (sMsg []byte, msgappid []byte) { + + // 2.validate signature + if !wp.ValidateSignature(sMsgSignature, sTimeStamp, sNonce, sEncryptMsg) { + return + } + + //3.decode base64 + sAesData, err := base64.StdEncoding.DecodeString(sEncryptMsg) + if err != nil { + return + } + + //4.decode aes + sAesKey := wp.GenAesKeyFromEncodingKey(constant.WX_NOTIFY_ENCODING_AES_KEY) + if sAesKey == "" { + return + } + sNoEncryptData := wp.AES_CBCDecrypt(sAesData, sAesKey) + + // 5. remove kRandEncryptStrLen str + if len(sNoEncryptData) <= constant.WX_RANDENCRYPT_STRLEN+constant.WX_KMSG_LEN { + return + } + netlenbyte := sNoEncryptData[constant.WX_RANDENCRYPT_STRLEN : constant.WX_RANDENCRYPT_STRLEN+constant.WX_KMSG_LEN] + buf := bytes.NewReader(netlenbyte) + iMsgLen := int(0) //ntohl(iNetLen); + binary.Read(buf, binary.BigEndian, &iMsgLen) + if len(sNoEncryptData) <= constant.WX_RANDENCRYPT_STRLEN+constant.WX_KMSG_LEN+iMsgLen { + return + } + sMsg = sNoEncryptData[constant.WX_RANDENCRYPT_STRLEN+constant.WX_KMSG_LEN : constant.WX_RANDENCRYPT_STRLEN+constant.WX_KMSG_LEN+iMsgLen] + + //6. validate appid + msgappid = sNoEncryptData[constant.WX_RANDENCRYPT_STRLEN+constant.WX_KMSG_LEN+iMsgLen:] + return +} + +func (wp *wxpay) ValidateSignature(sMsgSignature string, sTimeStamp string, sNonce string, sEncryptMsg string) bool { + sSignature := wp.ComputeSignature(constant.WX_NOTIFY_TOKEN, sTimeStamp, sNonce, sEncryptMsg) + if sSignature == "" { + return false + } + + return sMsgSignature == sSignature +} + +func (wp *wxpay) ComputeSignature(sToken string, sTimeStamp string, sNonce string, sMessage string) string { + if sToken == "" || sNonce == "" || sMessage == "" || sTimeStamp == "" { + return "" + } + + //sort + strs := []string{} + strs = append(strs, sToken) + strs = append(strs, sTimeStamp) + strs = append(strs, sNonce) + strs = append(strs, sMessage) + sort.Strings(strs) + sStr := strs[0] + strs[1] + strs[2] + strs[3] + + //compute + sha1crypto := sha1.New() + _, err := sha1crypto.Write([]byte(sStr)) + if err != nil { + return "" + } + + return hex.EncodeToString(sha1crypto.Sum(nil)) +} + +func (wp *wxpay) GenAesKeyFromEncodingKey(sEncodingKey string) string { + if len(sEncodingKey) != len(constant.WX_NOTIFY_ENCODING_AES_KEY) { + return "" + } + + sBase64 := sEncodingKey + "=" + data, err := base64.StdEncoding.DecodeString(sBase64) + if err != nil { + return "" + } + + return string(data) +} + +func (wp *wxpay) AES_CBCDecrypt(sSource []byte, sKey string) []byte { + if len(sSource) < constant.WX_AESKEY_SIZE || len(sSource)%constant.WX_AESKEY_SIZE != 0 { + return []byte{} + } + + key := []byte(sKey) + if len(sKey) > constant.WX_AESKEY_SIZE { + key = key[0:constant.WX_AESKEY_SIZE] + } + return aesDecryptCBC(sSource, key) +} + +func aesDecryptCBC(encrypted []byte, key []byte) (decrypted []byte) { + block, _ := aes.NewCipher(key) // 分组秘钥 + blockSize := block.BlockSize() // 获取秘钥块的长度 + blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) // 加密模式 + decrypted = make([]byte, len(encrypted)) // 创建数组 + blockMode.CryptBlocks(decrypted, encrypted) // 解密 + decrypted = pkcs5UnPadding(decrypted) // 去除补全码 + return decrypted +} + +func pkcs5UnPadding(origData []byte) []byte { + length := len(origData) + unpadding := int(origData[length-1]) + return origData[:(length - unpadding)] +}