# 出海小游戏支付开发者接入文档

# 介绍

出海小游戏当前仅支持游戏币能力,暂不支持道具直购能力。
游戏币定义见技术手册-虚拟支付篇
海外充值与国内有所不同,极端情况下充值成功后游戏币到账的延迟可能会较大,甚至需要用户重新拉起支付才能到账,所以建议开发者参考3.1.1.游戏币场景使用游戏币能力,把游戏币作为游戏内一种“点券”的概念,余额不足时,再拉起充值。

# 接口列表

  • [前端]查询商品本地价格:wx.getGamePaymentProductInfo
    • 与国内全部展示rmb价格不同,海外需要根据不同的环境展示对应的货币及价格,因此开发者在商品展示前需要调用该接口获取本地价格
    • 见本文档说明
  • [前端]支付:wx.requestGamePayment
    • 见本文档说明
  • [服务端]获取游戏币余额:pay_v2.getBalance
    • 见本文档说明
  • [服务端]扣除游戏币:pay_v2.pay
    • 见本文档说明
  • [服务端]给用户赠送游戏币:pay_v2.present
    • 见本文档说明

# 购买时序图

时序图
注意:

  1. 第4步购买前查询余额是否足够可以解决绝大多数掉单问题,现网有极小概率支付成功但前端收不到回调的情况。 比如:用户支付完成后强制杀掉微信或其他情况导致小程序异常退出,此时前端不可能收到回调,通过服务端的查询游戏币余额接口则可以查询到刚刚购买的游戏币。若游戏币数量足够,则不应再发起充值流程,除非当前场景明确是要求购买更多游戏币。
  2. 第11步支付成功后会有小概率延迟到账的情况,查询到的余额会和预期充值的对不上,这时需要提醒用户耐心等待,然后间隔一定时间后尝试重新查询
  3. 第13步需要选择合适的bill_no发起幂等扣除游戏币的请求,需要明确扣除成功(errcode为0)或者遇到逻辑失败(接口有正常的返回数据且errcode非-1)为止,90012 订单号重复代表相同的订单号已经处理过,不需要再重复调用。逻辑失败需要人工介入,比如传入的参数非接口预期,需要参考文档修正后再重新尝试
  4. 为了解决延迟到账或者前端wx.requestGamePayment回调丢失问题,除了前面提到的定期进行余额轮询,也可以接入事件订阅能力,通过主动接收推送和轮询结合的方式进行更可靠的发货:出海小游戏支付类订阅事件

# 支付请求签名算法说明

pay_sig参数的签名算法,使用“mp-支付基础配置”中的 AppKey 对支付的请求进行签名,代表请求经过开发者服务端的支付模块发起。签名算法伪代码为:

pay_sig = to_hex(hmac_sha256(app_key, uri + '&' + post_body))

可以参考以下python示例中的calc_pay_sig实现,其中:

  • uri为不带参数的API路径,如:/wxa/game/getbalance、/wxa/game/pay、/wxa/game/present
  • app_key为当前支付环境(env参数)对应的AppKey,从“mp-支付基础配置”处获取
  • post_body为该API(和uri对应)要求的原始http请求post的数据,参考具体接口的请求参数说明

signature参数签名算法参考用户登录态签名,也可以参考随后python示例中calc_signature实现。

# 特别提醒

  • POST的JSON数据序列化为字符串有无数种结果(紧凑型或者可读型,或者key有不同的顺序),只需要任意选一种即可。序列化为字符串(post_body)后再进行签名计算,并且真正发起HTTP请求时务必使用和签名时一致的字符串,否则可能出现签名错误。
  • 出现signature参数前面错误时,请先确保session_key有效,务必先阅读会话密钥 session_key 有效性一节说明。
  • 反面教材举例:使用Python把POST的JSON数据序列化为A,然后进行签名计算,得到SA,然而发起请求时,重新进行了JSON序列化(切换了语言或者使用同语言但不保证序列化的稳定性,得到字符串B),此时发起请求一定会得到签名错误。因为签名校验时是直接以HTTP请求中的POST Body作为post_body(即B)进行的计算,得到的签名和SA肯定是不一致的。所以切记保持签名时的post_body和发起HTTP请求时的POST Body一致。

以调用pay_v2.getBalance接口为例,签名计算如下:

#!/usr/bin/python
# -*- coding: utf-8 -*-
""" pay_sig签名算法计算示例 """

import hmac
import hashlib
import json
import time

def calc_pay_sig(uri, post_body, appkey):
    """ pay_sig签名算法
      Args:
          uri       - 当前请求的支付API的uri部分,不带query_string
                      例如:/wxa/game/getbalance、/wxa/game/pay
          post_body - http POST的数据包体
          appkey    - 对应环境的AppKey
      Returns:
          支付请求签名pay_sig
    """
    need_sign_msg = uri + '&' + post_body
    pay_sig = hmac.new(key = appkey.encode('utf-8'), msg = need_sign_msg.encode('utf-8'),
                       digestmod=hashlib.sha256).hexdigest()
    return pay_sig

def calc_signature(post_body, session_key):
    """ 用户登录态signature签名算法
      Args:
          post_body   - http POST的数据包体
          session_key - 当前用户有效的session_key,参考auth.code2Session接口
      Returns:
          用户登录态签名signature
    """
    need_sign_msg = post_body
    signature = hmac.new(key = session_key.encode('utf-8'), msg = need_sign_msg.encode('utf-8'),
                       digestmod=hashlib.sha256).hexdigest()
    return signature

# uri,切记不可带参数,即去掉"?"及后面的部分
# 其他值如: /wxa/game/pay、/wxa/game/cancelpay、/wxa/game/present
uri = '/wxa/game/getbalance'

# 此处appkey为假设值,实际使用应根据支付环境(env参数)替换为对应的AppKey
appkey = "12345"

# 注意:JSON数据序列化结果,不同语言/版本结果可能不同
# 所以示例为了保证稳定性,直接用其中一个序列化的版本
# 实际使用时只需要保证,参与签名的post_body和真正发起http请求的一致即可
"""
# ts需要设置为当前unix timestap(秒级)
# 实际使用时可参考: int(time.time())
# 此处写死方便稳定复现算法
ts = 1668136271

# 不同接口要求的Post Body参数不一样,此处以getBalance接口为例(和uri对应)
post_body = json.dumps({
    "openid": "oUrsfxxxxxxxxxx",
    "ts": ts,
    "zone_id": "1",
    "env": 0
})
"""
post_body = '{"openid": "oUrsfxxxxxxxxxx", "ts": 1668136271, "zone_id": "1", "env": 0}'

# step1. pay_sig签名计算(支付请求签名算法)
pay_sig = calc_pay_sig(uri, post_body, appkey)
print("pay_sig:", pay_sig)

# 若实际请求返回pay_sig签名不对,根据以下步骤排查:
# 1. 确认算法:uri、post_body、appkey写死以上参数,确保你的签名算法和示例calc_pay_sig结果完全一致
# 2. 确认参数:
#    - uri不可带参数(即"?"及后续部分全部舍去)
#    - post_body必须和真正发起HTTP请求的post body完全一致
#    - appkey必须是与请求中对应的环境匹配(env参数决定)
assert pay_sig == "bf60c1e7074fe27475887ea3d75dba5edb5ccfc071793a8b6d2fa144b0441a4d"

# step2. signature签名计算(用户登录态签名算法)
# session_key需要为当前用户有效session_key(参考auth.code2Session接口获取)
# 此处写死方便复现算法
session_key = "9hAb/NEYUlkaMBEsmFgzig=="
signature = calc_signature(post_body, session_key)
print("signature:", signature)

# 若实际请求返回signature签名不对,参考随后的“90010-signature签名错误问题排查思路”进行排查
assert signature == "e1b8723aaa8748221d8418f3538e59d7ba647b29cec612470fc204a78ba41cca"

# 90010-signature签名错误问题排查思路

# step 1. 确认算法是否正确

post_body、session_key 写死示例中的参数,确保你的签名算法和示例 calc_signature 结果完全一致。若不一致,需要修正算法。

# step 2. 确认参数是否正确

  • session_key必须是用户当前有效的session_key,可以通过服务端 auth.checkSessionKey 接口 检查是否有效:即使用session_key对空字符串进行签名。需要记住一点:只要前端调用过wx.login,则服务端的session_key就可能失效,需要使用最后一次wx.login获得的code去换取最新的session_key(通过服务端 auth.code2Session 接口 )。若只有一部分情况下出现签名错误,则大概率是wx.login的调用导致了session_key的失效,可以进一步参考session_key相关注意事项
  • post_body必须和真正发起HTTP请求的POST Body完全一致,部分语言或者http库会在真正发起请求时额外添加其他字符或者做一些字符转义,此时需要传入相关选项确保POST Body和签名时的post_body保持一致。可以通过以下方式确认post_body的数据是否有被修改:1)请求时本地抓包,直接看http请求的body;2)签名问题的接口回包中errmsg会带上本次请求的实际post_body,有两种形式:i) 使用中括号包裹的原始数据,去掉中括号即是(先从JSON中取出errmsg字符串,避免JSON转义影响);ii) base64编码的post_body,需要base64 decode获得。

# wx API

# wx.getGamePaymentProductInfo

用于查询当前用户运行地区的支付外显价格(含货币单位)。

// 接口请求参数
interface Options {
    env: number;    // 0 现网 1沙箱
    buyQuantityList: number[]; // 需要查询的游戏币档位,档位枚举值详见后面表格
}

// 接口成功的回调
interface Response {
    infos: {
        quantity: number;
        displayPrice: string;
    }[]
}

// 案例
wx.getGamePaymentProductInfo({
    env: 1,
    buyQuantityList: [
         99, 6999
    ]
}).then( res => {
    // res = { infos: [ { quantity: 99, displayPrice: "$99.00" }, { quantity: 6999, displayPrice: "$6999.00" } ] }
    console.log(res);
}).catch(err => {
    // 查询失败
    console.error(err);
})

# wx.requestGamePayment

使用方法参见,只是接口名做了修改 wx.wx.requestMidasPayment(Object object)|微信开放文档
对于海外充值,我们精简了部分参数,只需要以下参数

wx.requestGamePayment({
 env:1, //  0 现网 1 沙箱 暂时只支持沙箱测试
 zoneId:"1" // 暂时只配置了分区 1,不同分区的游戏币余额独立,需要新增请联系平台添加
 buyQuantity:99 // 游戏币档位,见本文档后面表格
 outTradeNo:"" //业务订单号,每个订单号只能使用一次,重复使用会失败。开发者需要确保该订单号在对应游戏下的唯一性,平台会尽可能校验该唯一性约束,但极端情况下可能会跳过对该约束的校验。要求32个字符内,只能是数字、大小写字母、符号_-|*组成,不能以下划线()开头。每次调用wx.requestGamePayment都换新的outTradeNo。若没有传入,则平台会自动填充一个,并以下划线开头
 success(res, errCode) {
     console.log('pay', res, errCode);
 },
 fail({
     errMsg,
     errCode
 }) {
     console.error(errMsg, errCode)
 }

# 服务端API

# 查询游戏币余额(pay_v2.getBalance)

查询游戏币余额。本接口开通了虚拟支付的小游戏可用。通过本接口查询某个用户的游戏币余额,查询时机可以是用户支付完成,或者用户查看游戏币余额等场景。注意,某些极端情况下,支付完成后可能余额会延迟到账,需要按一定间隔定期查询,并提示用户耐心等待。

# HTTPS调用

# 请求地址
POST https://univapi.wechat.com/wxa/game/getbalance?access_token=ACCESS_TOKEN&signature=SIGNATURE&sig_method=SIG_METHOD&pay_sig=PAY_SIGNATURE
# 请求参数
# Query参数
属性 类型 必填 说明
access_token string 接口调用凭证
signature string 用户登录态签名,签名算法请参考用户登录态签名算法
sig_method string 用户登录态签名的哈希方法,只支持hmac_sha256,请参考用户登录态签名算法
如:"hmac_sha256"
pay_sig string 支付请求签名,参考“支付请求签名算法说明
# POST Body
# Object

POST的JSON数据包

属性 类型 必填 说明
openid string 用户唯一标识符
ts number 当前UNIX时间戳(请尽可能确保时间准确),单位:秒
如:1668136271
zone_id string 已发布的分区ID(MP-分区配置-分区ID)
需要和env对应
env number 环境配置
0:现网环境(也叫正式环境)
1:沙箱环境
user_ip string 用户外网IP
# 返回值
# Object

返回的JSON数据包。 提示:若无特殊说明,带save的为有价(现金充值)相关统计,带present的为赠送相关的统计,都不带则为有价+赠送合并相关的统计。

属性 类型 说明
errcode number 错误码
errmsg string 错误信息
balance number 游戏币总余额,包括现金充值和赠送部分
present_balance number 赠送账户的游戏币余额(原1.0的gen_balance)
sum_save number 累计现金充值获得的游戏币数量(原1.0的save_amt)
sum_present number 累计赠送的游戏币数量(原1.0的present_sum)
sum_balance number 累计获得的游戏币数量,包括现金充值和赠送(原1.0的save_sum)
sun_cost number 累计总消耗(即扣除)游戏币数量(原1.0的cost_sum)
first_save bool 是否满足首充活动标记(原1.0的first_save)
# errcode的合法值
说明
0 请求成功
-1 系统繁忙,请开发者稍候再试
90010 signature签名错误或用户登录态(session_key)已过期
90011 pay_sig签名错误
90018 参数错误,具体参数见errmsg描述

# 扣除代币(pay_v2.pay)

扣除游戏币。本接口开通了虚拟支付的小游戏可用。通过本接口扣除某个用户的游戏币后发放游戏内的等值道具或提供等值的游戏服务。 由于可能存在接口调用超时、或返回系统繁忙(errcode为-1),此时订单状态未知(可能扣除成功,也可能扣除失败),此时可以用相同的bill_no再次调用本接口,直到明确返回成功(errcode为0)或者非系统繁忙(errcode不为-1,即其他的逻辑失败)为止。对相同的billno多次调用该接口保证操作幂等,不会重复扣除。也可以调用退回扣除游戏币接口(pay_v2.cancelPay)取消本次扣除。

# HTTPS调用

# 请求地址
POST https://univapi.wechat.com/wxa/game/pay?access_token=ACCESS_TOKEN&signature=SIGNATURE&sig_method=SIG_METHOD&pay_sig=PAY_SIGNATURE
# 请求参数
# Query参数
属性 类型 必填 说明
access_token string 接口调用凭证
signature string 用户登录态签名,签名算法请参考用户登录态签名算法
sig_method string 用户登录态签名的哈希方法,只支持hmac_sha256,请参考用户登录态签名算法
如:"hmac_sha256"
pay_sig string 支付请求签名,参考“支付请求签名算法说明
# POST Body
# Object

POST的JSON数据包

属性 类型 必填 说明
openid string 用户唯一标识符
ts number 当前UNIX时间戳(请尽可能确保时间准确),单位:秒
如:1668136271
zone_id string 已发布的分区ID(MP-分区配置-分区ID)
需要和env对应
env number 环境配置
0:现网环境(也叫正式环境)
1:沙箱环境
user_ip string 用户外网IP
amount number 扣除游戏币数量,需要大于0(原1.0的amt)
bill_no string 扣除游戏币订单号,业务需要保证全局唯一,相同的订单号的多次请求不会重复扣除;
长度不超过63,只能是数字、英文大小写字母及_-的组合;
不能以下划线(_)开头(2.0新增约束)
payitem string 道具信息
remark string 备注
auto_rety bool 对于超时的情况会自动重试,如若同一个 bill_no 扣除代币数量不一致会返回失败
# 返回值
# Object
属性 类型 说明
errcode number 错误码
errmsg string 错误信息
bill_no string 扣除游戏币订单号
balance number 扣款后的余额
used_present_amount number 本次扣的赠送币的数量(原1.0的used_gen_amt)
# errcode的合法值
说明
0 请求成功
-1 系统繁忙,请开发者稍候再试
90010 signature签名错误或用户登录态(session_key)已过期
90011 pay_sig签名错误
90012 订单号重复
90013 余额不足
90016 sessionkey fail,用户sessionkey过期,需要重走登录流程
90018 参数错误,具体参数见errmsg描述

# 赠送游戏币(pay_v2.present)

给用户赠送游戏币。本接口开通了虚拟支付的小游戏可用。通过该接口赠送游戏币给某个用户。

# HTTPS调用

# 请求地址
POST https://univapi.wechat.com/wxa/game/present?access_token=ACCESS_TOKEN&signature=SIGNATURE&sig_method=SIG_METHOD&pay_sig=PAY_SIGNATURE
# 请求参数
# Query参数
属性 类型 必填 说明
access_token string 接口调用凭证
signature string 用户登录态签名,签名算法请参考用户登录态签名算法
sig_method string 用户登录态签名的哈希方法,只支持hmac_sha256,请参考用户登录态签名算法
如:"hmac_sha256"
pay_sig string 支付请求签名,参考“支付请求签名算法说明
# POST Body
# Object

POST的JSON数据包

属性 类型 必填 说明
openid string 用户唯一标识符
ts number 当前UNIX时间戳(请尽可能确保时间准确),单位:秒
如:1668136271
zone_id string 已发布的分区ID(MP-分区配置-分区ID)
需要和env对应
env number 环境配置
0:现网环境(也叫正式环境)
1:沙箱环境
user_ip string 用户外网IP
amount number 赠送游戏币的个数,不能为0(原present_counts)
bill_no string 赠送订单号,业务需要保证全局唯一,相同的订单号多次请求保证幂等;
长度不超过63,只能是数字、英文大小写字母及_-的组合;
不能以下划线(_)开头
# 返回值
# Object

返回的JSON数据包

属性 类型 说明
errcode number 错误码
errmsg string 错误信息
bill_no string 赠送订单号
balance number 赠送后的余额
# errcode的合法值
说明
0 请求成功
-1 系统繁忙,请开发者稍候再试
90010 signature签名错误或用户登录态(session_key)已过期
90011 pay_sig签名错误
90012 订单号重复,即已存在相同bill_no的请求但其他参数不一致
90018 参数错误,具体参数见errmsg描述

# 游戏币档位

与国内计算方法一致,需满足 buyQuantity * 游戏币单价 = 限定的价格等级
海外有效价格等级如下,下面美元只是参考价格,用户实际支付时可能是韩元、港币等,与美元价格会有一定的浮动

美元(实际支付金额) 单位货币个数(buyQuantity)
0.99 99
1.99 199
2.99 299
4.99 499
9.99 999
10.99 1099
15.99 1599
19.99 1999
24.99 2499
25.99 2599
29.99 2999
49.99 4999
54.99 5499
69.99 6999
79.99 7999
89.99 8999
99.99 9999

当前游戏币单价为 0.01 美元,若用户实际支付为非美元时,会有一定浮动。

# 支付类订阅事件

开发者订阅后可以通过开放平台消息通道收到对应的事件通知(如游戏币发货成功/退款成功)。
海外支付类订阅事件文档