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

图1: wx小游戏支付流程
小游戏道具直购支付成功,发货回调
文档参考
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: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来验证, 实际传输数据会发post
if 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来验证, 实际传输数据会发post
w.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 string
switch payLoad.Env {
case 0: return 虚拟支付2.0-> 基本配置 -> 基础配置 -> 支付基础配置 -> 现网 AppKey
case 1: return 虚拟支付2.0-> 基本配置 -> 基础配置 -> 支付基础配置 -> 沙箱 AppKey
default: 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
服务器收到玩家进入客服会话推送
- 文档参考
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 - 配置
小程序管理后台 -> 开发 -> 开发管理 -> 消息推送配置
和道具发货推送配置一样的, 要配Url、Token、EncodingAESKey、数据加密方式、数据格式
** url 允许配置http开头,但必须选择安全模式 **
点击提交时,会发送Get, 以明文模式验签并返回echoStr,楼上已展示代码
服务器向玩家推送客服消息,携带图文链接
发送客服消息,需要 access_token, access_token 每2小时过期, 有次数限制
access_token根据appid和appsecret, 向wxsdk发送post请求拿到。我推荐stable_token

图5: stable_token 在有效期内多次获取,不会使原有的token失效。
想要客服会话中有图片,那么需要先上传图片资源。 小程序只允许上传临时资源,即你上传的资源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语法
- 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 - 代码
点击查看代码
// 收到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 // 小游戏原始ID
Encrypt 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 := WxSdkGuestMsgEncodingAESKey
encodingAESKey += "="
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
}
微信小游戏sdk接入支付和登录,解决了wx原生不支持ios支付的痛点的更多相关文章
- 微信小游戏爆款秘笈 数据库MongoDB攻略篇
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由腾讯云数据库 TencentDB 发表于云+社区专栏 随着微信小游戏的爆发,越来越多开发者关注到MongoDB与小游戏业务的契合度. ...
- 【转】微信小游戏接入Fundebug监控
在SegmentFault上看到Fundebug上线小游戏监控,刚好最近开始玩微信小游戏,于是尝试接入试了一下. 接入方法 创建项目的时候选择左下角的微信小游戏图标. 点击继续进入接入插件页面. 第三 ...
- 如何让微信小程序快速接入七牛云
如果你确定用七牛运行小程序的话,给大家分享一个九折优惠码:61d1fd4d1 月 9 日 微信小程序正式发布,小程序终于揭开了它神秘的面纱,开发者对小程序的追捧更是热度不减.从小程序的热门应用场景来看 ...
- 【转】利用 three.js 开发微信小游戏的尝试
前言 这是一次利用 three.js 开发微信小游戏的尝试,并不能算作是教程,只能算是一篇笔记吧. 微信 WeChat 6.6.1 开始引入了微信小游戏,初期上线了一批质量相当不错的小游戏.我在查阅各 ...
- Cocos Creator_发布到微信小游戏平台
观看官方教程,地址 传送门: http://docs.cocos.com/creator/manual/zh/publish/publish-wechatgame.html CocosCreator接 ...
- 用Kotlin破解Android版微信小游戏-跳一跳
前言 微信又更新了,从更新日志上来看,似乎只是一次不痛不痒的小更新.不过,很快就有人发现,原来微信这次搞了个大动作——在小程序里加入了小游戏.今天也是朋友圈被刷爆的缘故. 看到网上 有人弄了一个破解版 ...
- .Net Core ORM选择之路,哪个才适合你 通用查询类封装之Mongodb篇 Snowflake(雪花算法)的JavaScript实现 【开发记录】如何在B/S项目中使用中国天气的实时天气功能 【开发记录】微信小游戏开发入门——俄罗斯方块
.Net Core ORM选择之路,哪个才适合你 因为老板的一句话公司项目需要迁移到.Net Core ,但是以前同事用的ORM不支持.Net Core 开发过程也遇到了各种坑,插入条数多了也特别 ...
- 微信小游戏 50M那部分的缓存机制的使用
一.使用 AssetsManager 灵活定制微信小游戏的缓存策略 官网教程:http://developer.egret.com/cn/github/egret-docs/Engine2D/mini ...
- 三、微信小游戏开发 --- 小游戏API调用Platform
微信小游戏API Platform主要是Egret用于来调用平台的SDK的. 在Egret中使用接口定义Platform. Egret项目中默认的platform值是DebugPlatform. 发布 ...
- 一、微信小游戏开发 --- 初次在微信开发者工具里跑Egret小游戏项目
尝试下Egret的小游戏开发,学习,学习,干IT,不学习,就得落后啊... 相关教程: Egret微信小游戏教程 微信公众平台-微信小游戏教程 微信公众平台-微信小游戏接入指南 开发版本: Egret ...
随机推荐
- 【YashanDB知识库】swap空间使用超大报错
问题描述 问题单 使用GROUP_CONCAT函数时,数据库swap表空间上涨厉害 测试用例 drop table tmp1; create table tmp1(c1 int,c2 double,c ...
- AGC007F 题解
题意 给定两个长为 \(n\) 的字符串 \(S, T\),求最少进行多少次操作才能使 \(S = T\). 一次操作定义为:对于 \(i = 1, 2, .. n\),令第 \(i\) 位为操作后的 ...
- ARC119F 题解
前言 ARC119F 好厉害,是没见过的自动机 DP. 正文 [1] 分析 主要分析一下为什么这么写. [2] 状态设计 [3] 自动机状态转移 感觉状态设计中最难的就是如何处理带 \(O\) 的. ...
- C# HttpClient 基本使用方式(一)
.NetCore主要提供了HttpWebRequest,WebClient,HttpClient这三种访问web的方式,其中HttpWebRequest,WebClient都在官方被标注为已过时,如果 ...
- Angular 18+ 高级教程 – 大杂烩
前言 本篇记入一些 Angular 的小东西. Angular 废弃 API 列表 Docs – Deprecated APIs and features Using Tailwind CSS wit ...
- CSS – background and styling img
前言 之前写过一些: W3Schools 学习笔记 (2) – CSS Image Sprites W3Schools 学习笔记 (3) – CSS Styling Images & CSS ...
- docker 安装启动jenkins 以及问题剖析
docker 安装启动jenkins 以及问题剖析 首先,你环境必须要有docker,我这里是自己本地虚拟机Vmware,我的虚拟机时linux centos7的 .如果你不知怎么安装虚拟机和命令 ...
- dfs与贪心算法——洛谷5194
问题描述: 有n个砝码,将砝码从大到小排列,从第三个砝码开始,所有砝码均大于其前两个砝码之和,问怎样的砝码组合才可以组合出不大于c的最大重量,输出该重量 输入: 第一行输入两个个整数N,c,代表有N个 ...
- linux java 初始环境配置
linux初始环境配置 1.设置IP 查看虚拟机ip地址:ip addr 修改ip地址 Vi /etc/sysconfig/network~scrips/ifcfg-ens33(不一定是33 动态的) ...
- Android复习(四)权限—>请求应用权限
每款 Android 应用都在访问受限的沙盒中运行.如果应用需要使用其自己的沙盒外的资源或信息,则必须请求相应权限. 要声明您的应用需要某项权限,您可以在应用清单中列出该权限,然后在运行时请求用户批准 ...