微信小游戏sdk接入支付和登录,解决了wx原生不支持ios支付的痛点

news/2024/11/30 10:41:58/文章来源:https://www.cnblogs.com/yumianjiaolong/p/18559006

前情提要

微信小游戏是小程序的一种。
项目接入微信小游戏sdk的支付和登录。主要难点在于接入ios的支付。因为官方只支持android, 不支持ios。
即ios用户不能直接在小游戏中发起支付,参考市面上的wx小游戏,大都采用的是进入客服会话,客服发支付链接,ios玩家点击链接后拉起支付付款
wx的文档很多,但并没有在一块,本文档提供了接入wxsdk 各流程和相关链接。希望后来者接入不需要像我一样费力。
以下所有流程我自己都是跑通过的,无需担心。 此文章主要侧重于服务器部分的实现, 很多难写的地方, 我也贴上了Go代码。

wx小游戏 andorid 支付流程

        图1: wx小游戏支付流程

小游戏道具直购支付成功,发货回调

  1. 文档参考
    https://developers.weixin.qq.com/minigame/dev/guide/open-ability/virtual-payment/goods.html
    https://docs.qq.com/doc/DVUN0QWJja0J5c2x4?open_in_browser=true
    https://developers.weixin.qq.com/miniprogram/dev/framework/server-ability/message-push.html
    https://docs.qq.com/doc/DR1hhWlpnQXJXWHRh
    https://docs.qq.com/doc/DVWF6Z3dEVHJPWExn

  2. 配置

       图2:wx小游戏虚拟支付配置图
    

2.1 虚拟支付 2.0 > 直购配置 > 道具发货配置 > 开启推送, 点击提交会立即向配置的url 发送Http Get请求。 这是验证url是否可用
虚拟支付的道具发货配置推送,url只允许配置 https 开头
点击按钮 开始推送/提交时,会立即对配置的url 发送一条Get请求,服务器收到后需要返回url参数中的echoStr, 才能提交配置。
验证url可用的Get请求, 需要校验签名。 无论配置的明文模式还是安全模式,签名都按明文模式解析。 以下是Go版本的代码

点击查看小游戏道具直购发货推送Get验签代码
func receiveMsgNotify(tx *Tx, w http.ResponseWriter, r *http.Request) {// 第一次会发微信sdk会发Get来验证, 实际传输数据会发postif r.Method == http.MethodGet {replyVerifyUrl(tx, w, r)return}// 处理post请求
}// replyVerifyUrl 回复开启消息推送时验证Url可用的Get请求
func replyVerifyUrl(tx *Tx, w http.ResponseWriter, r *http.Request) {// 签名验证, 消息是否来自微信query := r.URL.Query()signature := query.Get("signature")timestamp := query.Get("timestamp")nonce := query.Get("nonce")echostr := query.Get("echostr")if !plainTextModeVerifySignature(signature, timestamp, nonce) {w.Write([]byte("fail"))}// 第一次会发微信sdk会发Get来验证, 实际传输数据会发postw.Write([]byte(echostr))
}
// plainTextModeVerifySignature 明文模式签名验证
func plainTextModeVerifySignature(signature, timestamp, nonce string) bool {// 签名验证, 消息是否来自微信strings := []string{timestamp, nonce, "你配置的Token"}sort.Strings(strings) // 进行字典型排序data := sha1.Sum([]byte(fmt.Sprintf("%s%s%s", strings[0], strings[1], strings[2])))encryptData := hex.EncodeToString(data[:])return encryptData == signature
}
2.2 开启道具直购推送后,还需要点击模拟推送, 返回值 需要为 {"ErrCode":0,"ErrMsg":"Success"}, 才算配置完成 收到Post请求 需要校验两次签名, 一次是楼上url参数中携带的签名,一次是 body中解析出来 PayEventSig字段的签名, 以下是Go版本的验签代码
点击查看小游戏道具直购推送Payload字段验签代码
func receiveMsgNotifyPost(tx *Tx, w http.ResponseWriter, r *http.Request) bool {ds, _:= io.ReadAll(r.Body)req := &YourStructName{}if err = json.Unmarshal(ds, req); err != nil {     // 明文模式body里的参数可以直接解, 安全模式的解法我放在最后return false}payLoad := YourPayLoadStructName{}if err = json.Unmarshal([]byte(req.MiniGame.Payload), payLoad); err != nil {return false}var appkey stringswitch payLoad.Env {case 0: return 虚拟支付2.0-> 基本配置 -> 基础配置 -> 支付基础配置 -> 现网 AppKeycase 1: return 虚拟支付2.0-> 基本配置 -> 基础配置 -> 支付基础配置 -> 沙箱 AppKeydefault: return false}createSign := createWeixinSdkSign(appkey, req.Event, req.MiniGame.Payload)return weixinreq.MiniGame.PayEventSig == createSign
}// 生成微信消息道具直购Post推送签名
func createWeixinSdkSign(app_key string, event, payload string) string {data := fmt.Sprintf("%s&%s", event, payload)hmacSha256ToHex := func(key, data string) string {mac := hmac.New(sha256.New, []byte(key))_, _ = mac.Write([]byte(data))bs := mac.Sum(nil)return hex.EncodeToString(bs[:])}return hmacSha256ToHex(app_key, data)
}

2.3 模拟发包验证成功后, 如图所示

  图3: 小程序虚拟支付道具直购推送成功开启

wx小游戏 ios 支付流程

  图4: wx小游戏 ios 支付流程

wx小程序下单

https://pay.weixin.qq.com/docs/merchant/apis/mini-program-payment/mini-prepay.html
推荐使用 wx github提供的接口, 自己看例子。Go版本开源代码在这里 https://github.com/wechatpay-apiv3/wechatpay-go?tab=readme-ov-file

服务器收到玩家进入客服会话推送

  1. 文档参考
    https://developers.weixin.qq.com/minigame/dev/guide/open-ability/customer-message/receive.html
    https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/access-token/auth.getStableAccessToken.html
    https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/kf-mgnt/kf-message/sendCustomMessage.html
    https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/kf-mgnt/kf-message/uploadTempMedia.html
    https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/kf-mgnt/kf-message/getTempMedia.html
  2. 配置
    小程序管理后台 -> 开发 -> 开发管理 -> 消息推送配置
    和道具发货推送配置一样的, 要配Url、Token、EncodingAESKey、数据加密方式、数据格式
    ** url 允许配置http开头,但必须选择安全模式 **
    点击提交时,会发送Get, 以明文模式验签并返回echoStr,楼上已展示代码

服务器向玩家推送客服消息,携带图文链接

  1. 发送客服消息,需要 access_token, access_token 每2小时过期, 有次数限制

  2. access_token根据appid和appsecret, 向wxsdk发送post请求拿到。我推荐stable_token

    图5: stable_token 在有效期内多次获取,不会使原有的token失效。
    
  3. 想要客服会话中有图片,那么需要先上传图片资源。 小程序只允许上传临时资源,即你上传的资源3天就会过期, 过期了就需要重新上传。

Go版本上传图片资源的代码
// url := fmt.Sprintf("%s?access_token=%s&type=%s", https://api.weixin.qq.com/cgi-bin/media/upload(官网上新增图片素材的url), 从wxsdk处获取到的access_token, "image")// httpUploadImage 图片过期了上传图片到wx服务器
func httpUploadImage(url, imagePath string, reply interface{}) error {body := &bytes.Buffer{}writer := multipart.NewWriter(body)file, err := os.Open(imagePath)if err != nil {return fmt.Errorf("imagepath illegal:%v, err:%v", imagePath, err)}defer file.Close()part, err := writer.CreateFormFile("media", imagePath)if err != nil {return fmt.Errorf("createFormFile err:%v", err)}_, err = io.Copy(part, file)if err != nil {return fmt.Errorf("io.copy err:%v", err)}writer.Close()// 我这里用的 "github.com/go-resty/resty/v2" 包, 用标准库的http一样的, Header 要手动改一下// 构建http请求resp, err := resty.New().SetTimeout(5*time.Second).R().SetHeader("Content-Type", writer.FormDataContentType()).SetBody(body).Post(url)if err != nil {return err}if !resp.IsSuccess() {return fmt.Errorf("http status code: %d", resp.StatusCode())}return json.Unmarshal(resp.Body(), reply)
}
3. 上传图片后得到 media_id, 发送给玩家客服消息中携带图文链接, url 是 game server 要提供的, Thumb_url: fmt.Sprintf("%s?access_token=%s&type=image&media_id=%s", "https://api.weixin.qq.com/cgi-bin/media/get"(官网上获取客服消息中的临时素材的url), 从wxsdk处获取到的access_token, 上传图片post请求返回值得到 media_id)

  图6:实操截图

玩家点击支付链接,服务器返回带小程序支付的html语法

  1. doc
    https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/jsapi-transfer-payment.html
    https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_4.shtml
  2. 代码
点击查看代码
// 收到ClickLink请求
func ClickLink(tx *Tx, w http.ResponseWriter, r *http.Request) {// balabala 校验代码nowStr := strconv.FormatInt(time.Now().Unix(), 10)packageStr := fmt.Sprintf("prepay_id=%s", "下单的时候存储的prepayid")nonceStr := generateRandomString() // 随机字符串长度最长不能超过32位, 这段代码很简单就不贴了paySign, err := createSign("小程序appID", nowStr, nonceStr, packageStr) // 参考github上支付写的if err != nil {log.Panicf("[%s]iosPayLinkcheck  createSign err:%v order sn %s", tx, err, sn)}reply := fmt.Sprintf(`<html>
<script>
function onBridgeReady() {WeixinJSBridge.invoke('getBrandWCPayRequest', {"appId": "%s","timeStamp": "%s","nonceStr": "%s","package": "%s","signType": "RSA","paySign":"%s"},function(res) {console.log(res.err_msg)if (res.err_msg == "get_brand_wcpay_request:ok") { // 支付成功document.write("payment success");WeixinJSBridge.call('closeWindow');}if (res.err_msg == "get_brand_wcpay_request:fail") { // 支付失败document.write("payment fail");WeixinJSBridge.call('closeWindow');}if (res.err_msg == "get_brand_wcpay_request:cancel") { // 支付取消document.write("payment cancel");WeixinJSBridge.call('closeWindow');}});}if (typeof WeixinJSBridge == "undefined") {if (document.addEventListener) {document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);} else if (document.attachEvent) {document.attachEvent('WeixinJSBridgeReady', onBridgeReady);document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);}} else {onBridgeReady();}
</script>
</html>`, WxSdkAppID, nowStr, nonceStr, packageStr, paySign)w.Header().Set("Content-Type", "text/html")w.Write([]byte(reply))http.Error(w, "", http.StatusOK)
}func createSign(appid, timeStamp, nonceStr, packageStr string) (string, error) {message := fmt.Sprintf("%s\n%s\n%s\n%s\n", appid, timeStamp, nonceStr, packageStr)// 加载私钥privateKey, err := utils.LoadPrivateKeyWithPath("小程序商户密钥的路径") //  官方开源提供的"github.com/wechatpay-apiv3/wechatpay-go/utils"if err != nil {return "", fmt.Errorf("load private payment key err:%v", err)}// 签名signature, err := signWithRsa(message, privateKey)if err != nil {return "", fmt.Errorf("generateSignature err:%v", err)}return signature, nil
}// 生成rsa签名
func signWithRsa(data string, privateKey *rsa.PrivateKey) (string, error) {// 使用 SHA256 对待签名数据进行哈希hash := sha256.New()hash.Write([]byte(data))hashed := hash.Sum(nil)// 使用私钥对哈希值进行 RSA 签名signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)if err != nil {return "", fmt.Errorf("failed to sign data: %v", err)}// 将签名进行 Base64 编码encodedSignature := base64.StdEncoding.EncodeToString(signature)return encodedSignature, nil
}

小程序支付成功,发货成功回调

终于走到这一步了!
https://pay.weixin.qq.com/docs/merchant/apis/mini-program-payment/payment-notice.html
同样, 使用官方开源库及例子书写
至此,wx小游戏ios支付已通

wx小游戏登录流程

   图7: wx 小游戏官方登录流程图

    图8: wx小游戏官方登录流程图(简化版)

后记

推送消息,如果使用安全模式, 官方并没有go版本的文档, 问了客服,也迟迟没回复,于是我自己写了一版。测试过能解析,但还是有点担心可能也有解析不到的特殊数据。

点击查看代码
// getMsgByPlainTextMode (安全)加密模式解析消息   官方没有提供go的写法,自己写的,可能有问题。
func getMsgBySafeMode(r *http.Request, req interface{}) error {query := r.URL.Query()signature := query.Get("msg_signature")timestamp := query.Get("timestamp")nonce := query.Get("nonce")ds, err := io.ReadAll(r.Body)if err != nil {return fmt.Errorf("io.ReadAll err %v", err)}type _WeiXinSDKGuestMsgSafeMode struct {ToUserName string // 小游戏原始IDEncrypt    string // 密文}encryptReq := &_WeiXinSDKGuestMsgSafeMode{}if err = json.Unmarshal(ds, encryptReq); err != nil {return fmt.Errorf("json.unmarshal err:%v ds:%v", err, string(ds))}if len(encryptReq.Encrypt) == 0 {return fmt.Errorf("encryReq.Encrypt is empty ")}if !safeModeVerifySignature(signature, timestamp, nonce, encryptReq.Encrypt) {log.Errorf("getMsgByPlainTextMode signature not match, signature:%v, timestamp:%v nonce:%v", signature, timestamp, nonce)return errors.New("signature not match")}// 漫长的解密步骤encodingAESKey := WxSdkGuestMsgEncodingAESKeyencodingAESKey += "="aesKey, err := base64.StdEncoding.DecodeString(encodingAESKey)if err != nil {return err}tmpMsg, err := base64.StdEncoding.DecodeString(encryptReq.Encrypt)if err != nil {return err}// 使用 AES 解密fullStr, err := aesDecryptCBC(tmpMsg, aesKey)if err != nil {return err}msg := fullStr[20:]ret := strings.Split(string(msg), "}")if len(ret) == 0 {return errors.New("msg is empty")}ret[0] += "}"if err = json.Unmarshal([]byte(ret[0]), req); err != nil {return fmt.Errorf("json.unmarshal err:%v ds:%v", err, string(ds))}return nil
}func aesDecryptCBC(cipherText, key []byte) ([]byte, error) {block, err := aes.NewCipher(key)if err != nil {return nil, fmt.Errorf("failed to create AES cipher: %v", err)}// 获取 AES 块大小blockSize := block.BlockSize()// 确保密文长度是块大小的整数倍if len(cipherText)%blockSize != 0 {return nil, fmt.Errorf("ciphertext is not a multiple of block size")}// 使用 CBC 模式解密mode := cipher.NewCBCDecrypter(block, key[:blockSize]) // CBC 模式,IV 是密钥的一部分plainText := make([]byte, len(cipherText))mode.CryptBlocks(plainText, cipherText)// 去除填充plainText, err = pkcs7UnPadding(plainText)if err != nil {return nil, fmt.Errorf("failed to remove padding: %v", err)}return plainText, nil
}// 去掉 PKCS#7 填充
func pkcs7UnPadding(data []byte) ([]byte, error) {length := len(data)if length == 0 {return nil, fmt.Errorf("data is empty")}// 获取填充字节的大小padding := int(data[length-1])if padding > length {return nil, fmt.Errorf("invalid padding")}return data[:length-padding], nil
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/840939.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vxe-form table 表单修改数据校验错误提示的样式

官网:https://vxeui.com<template><div><vxe-formv-bind="formOptions"@submit="submitEvent"@reset="resetEvent"></vxe-form></div> </template><script> import { VxeUI } from vxe-pc-uiexport …

vxe-form table 设置表单上下布局

官网:https://vxeui.com<template><div><vxe-formv-bind="formOptions"@submit="submitEvent"@reset="resetEvent"></vxe-form></div> </template><script> import { VxeUI } from vxe-pc-uiexport …

LeetCode19 删除链表的倒数第 N 个结点

删除链表的倒数第 N 个结点LeetCode19 删除链表的倒数第 N 个结点 题目链接:LeetCode19 描述 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。示例输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]思路定义fast指针和slow指针,初始值为虚拟头结点 fast首先…

法律咨询行业知识库构建:数字化与智能工具的应用

在法律咨询行业中,构建高效、准确的知识库对于提升服务质量、优化客户体验至关重要。随着数字化转型的推进,数字化知识库已成为法律咨询行业发展的重要方向。本文将探讨法律咨询行业知识库构建的重要性、挑战,并介绍如何利用“HelpLook”工具实现数字化知识库的构建。 一、法…

功能齐全的jquery图片查看器插件

lightgallery.js是一款功能齐全的jquery图片查看器插件。该jquery图片查看器插件采用响应式设计,提供图片查看,图片轮播,图片放大等功能。它的特点还有:演示 下载以模态窗口的形式全屏展示图片。 显示图片的标题。 可以实现图片的懒加载。 可以自动播放图片,并提供进度条…

【转】cesium之添加天地图

转自 https://www.cnblogs.com/s313139232/p/16351468.html 公共参数:var token = 7b56038c276128a86a5b946404bf4df4;// 服务域名var tdtUrl = https://t{s}.tianditu.gov.cn/;// 服务负载子域var subdomains=[0,1,2,3,4,5,6,7];添加矢量底图:let layer1 = new Cesium.WebMa…

数据集编排加速介绍

1. 模型训练&存储的基本概念 1.1 M-P神经元模型 在生物神经网络中,每个神经元与其他神经元相连,当它“兴奋”时,就会向相连的神经元发送化学物质,从而改变这些神经元内的电位;如果某神经元的电位超过了一个“阈值”,那么它就会被激活,即“兴奋”起来,向其他神经元发…

鸿蒙NEXT开发案例:亲戚关系计算器

【引言】 在快节奏的现代生活中,人们往往因为忙碌而忽略了与亲戚间的互动,特别是在春节期间,面对众多的长辈和晚辈时,很多人会感到困惑,不知道该如何正确地称呼每一位亲戚。针对这一问题,我们开发了一款基于鸿蒙NEXT平台的“亲戚关系计算器”应用,旨在帮助用户快速、准确…

HCIA-07 OSPF基础

初步了解OSPF的基本概念、应用场景和基础配置。临界关系建立过程、DR与BDR的选举。OSPF的三大表:邻居表、LSDB表、OSPF表;五大报文:Hello报文、Database Description报文、Link State Request报文、Link State Update报文、Link State ACK报文。目录 1-OSPF协议概述 1.1 LAS…

PG优化-业务场景需求实现-大表低基数列group by优化

PG优化-业务场景需求实现-大表低基数列group by优化 原创 akengan DB印象 2021年11月07日 22:18阅读使人充实,讨论使人敏捷,写作使人精确。前言今天抽空聊一个和成本优化相关的话题。说到成本优化,大家觉得优化多少算不错呢?10%? 20%? 成本优化的空间到底有多大呢?今天…

5大核心技术详解:AI视频监控系统背后的工作原理

随着人工智能的发展,AI视频监控技术成为当前视频监控行业的一大革新。传统监控系统往往依赖人工实时查看,效率低且容易遗漏,而AI赋能的视频监控系统可以通过图像识别、深度学习等技术实现自动化、智能化。本文将从技术层面对AI视频监控的五大核心组成部分进行详细解析。高效…

KingbaseES V8R6集群备份恢复案例之---远程外部备份故障案例

KingbaseES 、备份恢复案例说明: KingbaseES V8R6集群,通过‘cluster’模式执行远程异地的外部备份,在备份初始化时,故障显示,远程节点连接数据库失败,异常终止。原因是,数据库节点不支持本地数据库访问导致。 适用版本:KingbaseES V8R6 集群架构:一、问题现象 如下所…