# 虚拟支付

# 一、产品介绍

为保障用户权益,提高交易安全,开发者在小程序内提供的虚拟商品(包括虚拟货币、解锁功能、订阅内容、付费功能、打赏、购买虚拟礼物等),购买和支付均需接入小程序虚拟支付;开发者可通过小程序管理后台--虚拟支付模块,开通一个新的微信支付商户号,使用账单查询、资金提现等服务。平台将以通过该支付能力产生的支付金额为结算基数,按一定费率收取技术服务费。

本文档主要介绍虚拟支付的核心基础流程,适用于安卓、鸿蒙、windows端,苹果iOS端有额外开通、适配流程,需依照相关文档处理。

若测试请先使用沙箱环境,若使用现网环境,平台将收取相关技术服务费。

安卓、鸿蒙、windows端:开发者请在2.19.2或以上版本基础库中调用。

苹果iOS端:开发者请在微信客户端8.0.68或以上版本调用

# 1.1 开通条件

  • 主体类型为已认证小程序
  • 小程序类型为企业/事业单位/个体工商户小程序
  • 小程序主体信息完备(若有缺陷请使用平台提供的修改链接修正)

# 1.2.开通入口

当小程序完成资质申请后,点击mp左边栏【虚拟支付】即可前往开通页面进行开通。

若当前账户满足开通条件后,点击【开通】按钮进行协议签署及资料上传,开通新的商户号。

# 1.3.开通流程

# 第一步:阅读协议

进行协议阅读虚拟支付协议,勾选【我已阅读并同意上述条款】后进入下一步

# 第二步:提交商户相关资料开通商户号

填写企业的营业执照,提现账户及支付管理员

# 第三步:账户状态查询及资料审核

完成商户资料提交,此时可在界面等待查询账户状态,审核一般需要1-7个工作日,审核状态下次进入时即可查看。

# 第四步:进行账户验证

进行账户验证(若支付管理员配置为公司法人则跳过该步骤)

# 第五步:扫码签约

扫码完成签约,签约完成后,资料将提交进行审核,开发者可离开当前页面。

注意由于"账户验证","签约"不一定会即时完成,开发者可退出页面,后续再次进入将继续延续之前的配置。

等待约1-2个工作日后回到虚拟支付模块,可以看到状态为已签约,或直接进入了商户管理的模块则表示已完成了签约,成功开通了二级商户号。

# 第六步:进入商户管理后台进行账单/订单查询,代币/道具配置及资金管理

当完成了前续动作后,虚拟支付栏目将呈现为全新的商户管理模块,我们搭建了开发者的商户后台,允许开发者在后台进行基础信息配置,代币及道具配置和管理(开发者可以自行选择使用道具或者代币来管理自己的商品)

详细的功能解释如下:

# 基本配置
  1. 基础配置:开发者可在该界面查看自身基础配置信息(包括appid/offerid/appkey,发货订阅等)

  2. 代币配置:开发者可在该界面配置代币,输入代币名称并且设置代币兑换比例进行保存 (请注意一旦发布无法修改!请注意代币名称符合相关法律法规要求)

  3. 道具管理:开发者可在该界面上传道具至开发版本及发布至现网版本,可支持编辑保存后发布,并且可以查看发货推送配置 (输入的道具名称需符合相关法律法规要求)

# 资金管理:

可在此界面进行账户余额查询和提现,查看每日账单具体情况,包括订单号,开发者收益等,也可以查看账单明细(后期将支持下载功能)

注:待结算金额指的系尚未进行分账的金额,未扣除技术服务费

结算周期:T+3,即开发者完成一笔订单后,资金将会冻结,3日后进行分账

# 交易订单

可在此界面查询订单状况,包括代币/道具订单的交易及发货情况,可进行退款操作(后期将支持下载功能)

# 广告金

平台将设置广告金政策以支持开发者的广告投放,商户可在虚拟支付--广告金管理界面查看平台向商户赠送的广告金。

# 二、开发流程

# 2.1 时序图

# 道具直购流程图

注意事项

  • 【7. 用户支付成功】: 由wx.requestVirtualPayment的success回调触发,可能会丢失,比如微信异常退出
  • 【发货推送分支】和【发货轮训分支】至少实现一个,两个结合的方式,可提供更可靠的道具直购体验

# 代币充值流程图

注意事项

  • 【10. 用户支付成功】 : 由wx.requestVirtualPayment的success回调触发,可能会丢失,比如微信异常退出
  • 【支付推送分支】和【支付轮训分支】至少实现一个,两个结合起来会更可靠

# 2.2 微信 API

基础库接口wx.requestVirtualPayment用于发起米大师虚拟支付,内部会包含下单,拉起支付逻辑

# 2.3.服务器API

接口名称 请求路径 描述
查询代币余额 /xpay/query_user_balance 本接口用于查询代币余额
扣减代币 /xpay/currency_pay 本接口用于扣减代币(一般用于代币支付)
查询创建的订单 /xpay/query_order 本接口用于查询创建的订单(现金单,非代币单)
代币支付退款 /xpay/cancel_currency_pay 本接口用于代币支付退款(currency_pay接口的逆操作)
通知已发货完成 /xpay/notify_provide_goods 通知已经发货完成(只能通知现金单),正常通过xpaygoodsdeliver_notify消息推送返回成功就不需要调用这个api接口
代币赠送 /xpay/present_currency 代币赠送接口,由于目前不支持按单号查赠送单的功能,所以当需要赠送的时候可以一直重试到返回0或者返回268490004(重复操作)为止
下载小程序账单 /xpay/download_bill 用于下载小程序账单,第一次调用触发生成下载url,可以间隔轮训来获取最终生成的下载url
启动订单退款任务 /xpay/refund_order 对使用jsapi接口下的单进行退款,此接口只是启动退款任务成功,启动后需要调用query_order接口来查询退款单状态,等状态变成退款完成后即为最终成功
创建提现单 /xpay/create_withdraw_order 本接口用于创建提现单
查询提现单 /xpay/query_withdraw_order 本接口用于查询提现单
批量上传道具 /xpay/start_upload_goods 启动批量上传道具任务,一次仅支持上传一个道具,多个道具需分多次请求
查询批量上传道具任务 /xpay/query_upload_goods 本接口用于查询批量上传道具任务
启动批量发布道具任务 /xpay/start_publish_goods 启动批量发布道具任务,一次仅支持发布一个道具,多个道具需分多次请求
查询批量发布道具任务 /xpay/query_publish_goods 本接口用于查询批量发布道具任务
查询商家账户可提现余额 /xpay/query_biz_balance 查询商家账户里的可提现余额
查询广告金充值账户 /xpay/query_transfer_account 本接口用于查询广告金充值账户
查询广告金发放记录 /xpay/query_adver_funds 本接口用于查询广告金发放记录
充值广告金 /xpay/create_funds_bill 本接口用于充值广告金
绑定广告金充值账户 /xpay/bind_transfer_accout 本接口用于绑定广告金充值账户
查询广告金充值记录 /xpay/query_funds_bill 本接口用于查询广告金充值记录
查询广告金回收记录 /xpay/query_recover_bill 本接口用于查询广告金回收记录
获取投诉列表 /xpay/get_complaint_list 本接口用于获取投诉列表
获取投诉详情 /xpay/get_complaint_detail 本接口用于获取投诉详情
获取协商历史 /xpay/get_negotiation_history 本接口用于获取协商历史
回复用户 /xpay/response_complaint 本接口用于回复用户
完成投诉处理 /xpay/complete_complaint 本接口用于完成投诉处理
上传媒体文件 /xpay/upload_vp_file 本接口用于上传媒体文件(如图片,凭证等)
获取微信支付反馈投诉图片的签名头部 /xpay/get_upload_file_sign 本接口用于获取微信支付反馈投诉图片的签名头部
下载广告金对应的商户订单信息 /xpay/download_adverfunds_order 本接口用于下载广告金对应的商户订单信息

# 签名详解

用户态签名和支付签名在基础库wx.requestVirtualPayment和服务器API中都会涉及

# 支付签名

签名算法伪代码为:

paySig = to_hex(hmac_sha256(appKey,uri + '&' + signData))
参数 wx API 服务器API
appKey 可通过小程序MP查看:虚拟支付 -> 基本配置 -> 基础配置中的沙箱AppKey和现网AppKey。注意:记得根据env值选择不同AppKey,env = 0对应现网AppKey,env = 1对应沙箱AppKey 同理
signData 基础库的signData字段 api的post body
uri 固定填requestVirtualPayment 举例:对于https://api.weixin.qq.com/xpay/query_user_balance来说,uri = /xpay/query_user_balance

可参考下面的calc_pay_sig函数

# 用户态签名

签名算法伪代码为:

signature = to_hex(hmac_sha256(sessionKey,signData))
参数 wx API 服务器API
sessionKey session_key 同理
signData 基础库的signData字段 api的post body

# 参考的python脚本

以调用query_user_balance接口为例,签名计算如下:

#!/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 例如:/xpay/query_user_balance
          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,切记不可带参数,即去掉"?"及后面的部分
# 如果是基础库的wx.requestVirtualPayment,uri固定为requestVirtualPayment
uri = '/xpay/query_user_balance'

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

# 注意:JSON数据序列化结果,不同语言/版本结果可能不同
# 所以示例为了保证稳定性,直接用其中一个序列化的版本
# 实际使用时只需要保证,参与签名的post_body和真正发起http请求的一致即可
"""
# 不同接口要求的Post Body参数不一样,此处以query_user_balance接口为例(和uri对应)
post_body = json.dumps({
    "openid": "xxx",
    "user_ip": "127.0.0.1",
    "env": 0
})
"""
post_body = '{"openid": "xxx", "user_ip": "127.0.0.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 == "c37809f27c6d7fd1837ad2500a04512b66b34fd793a39a385fade56dca89a4b5"

# 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 == "089d9e8dc5d308977360c4b79ec600a93d736802802a807d634192328032f6c7"

# 2.4 消息推送

# 推送响应格式说明

注:如果推送响应格式不对,微信会重新推送,最多试15次

目前支持三种方式:

  1. 【推荐】文档中列的ErrCode方式

    推送内容为xml格式时,响应内容需要是xml格式

    <xml><ErrCode>0</ErrCode><ErrMsg><![CDATA[success]]></ErrMsg></xml>
    
  2. 同理推送内容为json格式时,响应内容需要是json格式

    {"ErrCode":0,"ErrMsg":"success"}
    
  3. 响应内容为空 或者success,表示成功(等价于第一种的ErrCode = 0)

# 道具发货推送xpay_goods_deliver_notify

# 请求参数

字段 类型 说明
ToUserName String 小程序原始ID
FromUserName String 该事件消息的openid,道具发货场景固定为微信官方的openid
CreateTime Number 消息发送时间
MsgType String 消息类型,固定为:event
Event String 事件类型
xpay_goods_deliver_notify
OpenId String 用户openid
OutTradeNo String 业务订单号
Env Number 环境配置
0:现网环境(也叫正式环境)
1:沙箱环境
WeChatPayInfo Object 微信支付信息 非微信支付渠道可能没有
GoodsInfo Object 道具参数信息
TeamInfo Object 拼团信息

WeChatPayInfo内容如下:

字段 类型 说明
MchOrderNo String 微信支付商户单号
TransactionId String 交易单号(微信支付订单号)
PaidTime Number 用户支付时间,Linux秒级时间戳

GoodsInfo内容如下:

字段 类型 说明
ProductId String 道具ID
Quantity Number 数量
OrigPrice Number 物品原始价格 (单位:分)
ActualPrice Number 物品实际支付价格(单位:分)
Attach String 透传信息

TeamInfo内容如下:

字段 类型 说明
ActivityId String 活动id
TeamId String 团id
TeamType Number 团类型1-支付全部,拼成退款
TeamAction Number 0-创团 1-参团

# 返回参数

字段 类型 是否必填 说明
ErrCode Number 发送状态。0:成功,其他:失败
ErrMsg String 错误原因,用于调试。在errcode非0 的情况下可以返回

# 代币支付推送xpay_coin_pay_notify

# 请求参数

字段 类型 说明
ToUserName String 小程序原始ID
FromUserName String 该事件消息的openid,道具发货场景固定为微信官方的openid
CreateTime Number 消息发送时间
MsgType String 消息类型,固定为:event
Event String 事件类型
xpay_coin_pay_notify
OpenId String 用户openid
OutTradeNo String 业务订单号
Env Number 环境配置0:现网环境(也叫正式环境) 1:沙箱环境
WeChatPayInfo Object 微信支付信息 非微信支付渠道可能没有
CoinInfo Object 代币参数信息

WeChatPayInfo内容如下:

字段 类型 说明
MchOrderNo String 微信支付商户单号
TransactionId String 交易单号(微信支付订单号)
PaidTime Number 用户支付时间,Linux秒级时间戳

CoinInfo内容如下:

字段 类型 说明
Quantity Number 数量
OrigPrice Number 物品原始价格 (单位:分)
ActualPrice Number 物品实际支付价格(单位:分)
Attach String 透传信息

# 返回参数

字段 类型 是否必填 说明
ErrCode Number 发送状态。0:成功,其他:失败
ErrMsg String 错误原因,用于调试。在errcode非0 的情况下可以返回

# 退款推送xpay_refund_notify

# 请求参数

字段 类型 说明
ToUserName String 小程序原始ID
FromUserName String 该事件消息的openid,道具发货场景固定为微信官方的openid
CreateTime Number 消息发送时间
MsgType String 消息类型,固定为:event
Event String 事件类型
xpay_refund_notify
OpenId String 用户openid
WxRefundId String 微信退款单号
MchRefundId String 商户退款单号
WxOrderId String 退款单对应支付单的微信单号
MchOrderId String 退款单对应支付单的商户单号
RefundFee Number 退款金额,单位分
RetCode Number 退款结果,0为成功,非0为失败
RetMsg String 退款结果详情,失败时为退款失败的原因
RefundStartTimestamp Number 开始退款时间,秒级时间戳
RefundSuccTimestamp Number 结束退款时间,秒级时间戳
WxpayRefundTransactionId String 退款单的微信支付单号
RetryTimes Number 重试次数,从0开始。重试间隔为2 4 8 16...最多15次
TeamInfo Object 拼团信息

TeamInfo内容如下:

字段 类型 说明
ActivityId String 活动id
TeamId String 团id
TeamType Number 团类型1-支付全部,拼成退款
TeamAction Number 0-创团 1-参团

# 返回参数

字段 类型 是否必填 说明
ErrCode Number 发送状态。0:成功,其他:失败
ErrMsg String 错误原因,用于调试。在errcode非0 的情况下可以返回

# 用户投诉推送xpay_complaint_notify

# 请求参数

字段 类型 说明
ToUserName String 小程序原始ID
FromUserName String 该事件消息的openid,道具发货场景固定为微信官方的openid
CreateTime Number 消息发送时间
MsgType String 消息类型,固定为:event
Event String 事件类型
xpay_complaint_notify
OpenId String 用户openid
WxOrderId String 微信单号
MchOrderId String 商户单号
TransactionId String 微信支付交易单号
ComplaintId String 投诉单号
ComplaintDetail String 投诉详情
ComplaintTime Number 投诉时间,秒级时间戳
RetryTimes Number 重试次数,从0开始。重试间隔为2 4 8 16...最多15次
RequestId String 请求编号

# 返回参数

字段 类型 是否必填 说明
ErrCode Number 发送状态。0:成功,其他:失败
ErrMsg String 错误原因,用于调试。在errcode非0 的情况下可以返回

# 2.5.Windows客户端支付能力

能力介绍:目前腾讯旗下的媒体均可支持播放短剧小程序,C端用户可以在浏览Windows客户端媒体时观看短剧,并完成支付流程。

# 接入步骤:

1.调用 wx.getSystemInfo 或 wx.getDeviceInfo 接口,获取 platform 字段

2.兼容 "windows"平台也走到支付接口

3.使用微信开发者工具 - 真机调试能力,在 Windows客户端测试即可

//代码示例参考如下:支付方法同时兼容android和windows操作系统
if(wx.getSystemInfoSync().platform == 'android' || wx.getSystemInfoSync().platform == 'windows') {
    wx.requestVirtualPayment({...})
}

Windows 客户端正常支付通路如下:

开发者未适配Windows客户端的异常通路示意如下: