主题
Skip to content 
说明
支付模块提供了完善的支付功能
在接入支付模块之前,请确保已经和商务对接,申请到了各个市场的支付商品 ID
部分市场在申请商品 ID 之前,需要出一个申请支付 ID 的市场包提审,审核通过后才能申请到 ID
可以参考示例中的内容,来集成支付模块
核心模块位于 ZMYSDKManager 中
支付模块的一些概念
- 商品按照类型分为:消耗性道具、非消耗性道具、订阅类型道具
c#
public enum ProductType
{
/// 非消耗性道具
NoConsumable = 0,
/// 消耗性道具
Consumable = 1,
/// 订阅类型道具
subscription = 2,
}
- 从商品获得的来源可以分为:
c#
/// 商品获取渠道
public enum CommdityChannel
{
/// 购买渠道
Buy = 0,
/// 复送渠道
FixOrders,
/// 失败订单渠道
FailedOrders,
/// 恢复购买渠道
RestoreOrders,
}
正常购买(Buy)第一次,支付购买获得
复送订单(FixOrders)主动给用户复送订单,比如给用户发放补偿等
失败订单(FailedOrders)购买后还未发货的订单,应用下次启动会自动触发补单
恢复购买订单(RestoreOrders)用户主动触发,仅能用来恢复非消耗型商品权益
如果存在非消耗型商品携带了消耗道具的情况(如去广告礼包,赠送一定数量金币),请在发货接口里面判断,如果发货此商品来源是恢复购买,只发放去广告权益,不要发放消耗型道具。
订阅无法通过正常购买以外的其他渠道获得,仅能通过查询订阅信息来决定是否发放权益。
内购使用前的必须步骤
内购模块在使用前,需要先初始化内购模块;
c#
ZMYSDKManager.I.InitIap
SDK 需要得知目前游戏的商品信息,以 List<ProductData>数据结构传递给 SDK:
示例
c#
/// 金币-消耗型商品
private const string globKey = "com.test.test.test.pigbank";
/// vip--非消耗型商品
private const string vipKey = "com.test.test.test.noad";
/// 月卡--订阅型商品
private const string monthlyCardKey= "test.tset";
//使用内购前,要先初始化
ZMYSDKManager.I.InitIap(new List<ProductData>()
{
new ProductData(){productID = globKey,productType = ProductType.Consumable},//金币
new ProductData(){productID = vipKey,productType = ProductType.NoConsumable},//vip
new ProductData(){productID = monthlyCardKey,productType = ProductType.subscription},//月卡
});
WARNING
💡 内购中涉及到的 productID,都应该在初始化时告知 SDK
初始化完成,即可开始内购功能使用
Unity常用接口
购买
c#
/// 请求购买商品
/// <param name="productID">商品ID</param>
/// <param name="ext">预留扩展信息</param>
/// <param name="testIsSucceed">编辑器下是否购买成功</param>
ZMYSDKManager.I.RequestBuyProduct(string productID, string ext = "", bool testIsSucceed = true);
Iap\_ShowLoading
事件
c#
ZMYSDKManager.I.Iap_ShowLoading += ShowLoading;
发起购买后会触发Iap\_ShowLoading
事件,这是个全屏遮罩事件,业务研发应该监听此事件,并且阻断用户进一步操作,等待购买完成。
Iap\_CloseLoading
事件
c#
ZMYSDKManager.I.Iap_CloseLoading += CloseLoading;
购买流程结束后触发Iap\_CloseLoading
,业务方应该监听此事件,关闭全屏遮罩.
发货接口
根据商品类型不同,有 3 个发货接口
消耗性商品:
c#
//商品获取渠道,产品ID
Action<CommdityChannel, string> Iap_ReceiveConsumableCommodity
非消耗性商品:
c#
//商品获取渠道,产品ID
Action<CommdityChannel, string> Iap_ReshNonConsumableState
订阅商品:
c#
Action<NonConsumableState> Iap_ReshSubscriptionState
c#
public struct NonConsumableState
{
/// 产品ID
public string productId;
/// 状态0-查询失败 1-订阅中 2-订阅过期
public int state;
/// 获取渠道
public CommdityChannel channel;
/// 到期事件
public string expiresDate;
}
WARNING
💡 当订阅商品的 state 为 0 时,不要做任何处理。保持之前的商品状态记录
恢复购买
WARNING
💡 含有非消耗型内购的项目,需要在设置面加上恢复购买
按钮
恢复当前已经购买的非消耗性道具、订阅商品。(注意:消耗性道具不能恢复)
c#
ZMYSDKManager.I.RequestRestoreBuy();
恢复到的结果,会以发货接口发送给业务方
非消耗商品的消耗包处理
由于非消耗商品只能购买一次,所以测试经常会没法测试非消耗商品的购买流程,为此我们需要打一个专门用来消耗非消耗商品的包,启动消耗后,再装正式包即可再次购买非消耗商品。
消耗包处理方式:
在模板工程的src/com/adv/core/AdsConstant.java文件中,添加以下代码
java
public final static boolean PayConsume = true;
测试流程:
再次购买非消耗商品,会有 PayModule-PayManagerGoogle-GooglePayUtil !!!!!开启非消耗商品消耗包模式!!!!!
样式日志打印
WARNING
💡 添加代码打包后,记得及时去除,不要将此配置带到正式包内。
订阅查询
查询订阅产品信息
c#
ZMYSDKManager.I.RequestCheckSubscription(string productId);
查询到的结果,会以发货接口发送给业务方
失败、复送、服务器赠送道具订单
SDK 会在UpdateIap
方法中轮询调用查询失败订单、复送订单和服务器赠送道具订单接口,业务方不用处理。
查询到的结果,会以发货接口发送给业务方,业务方需要下发道具。
获取商品信息
此接口不宜调用太早,推荐是在即将展示商品时再调用,获取的是支付sdk初始化之后缓存到本地的数据,所以可以多次高频率调用。
c#
ZMYSDKManager.I.GetProductInfo(string productId);
c#
/* {
"sku": "com.test.test.test.pigbank",
"price": "2.99",
"title": "Piggy Bank",
"priceCountryApiType": "US$",
"priceCountryApiCode": "USD",
"description": "Offers up to 3200 coins upon purchasing."*/
public class ProductInfo
{
///商品ID
public string sku;
/// 标题
public string title;
/// 描述
public string description;
/// 价格
public string price;
/// 货币的符号
public string priceCountryApiType;
/// 币种
public string priceCountryApiCode;
/// 货币符号
public string Currency
{
get
{
if (string.IsNullOrEmpty(priceCountryApiType))
{
return priceCountryApiCode;
}
return priceCountryApiType;
}
}
}
pay_show 上报
当内购商品展示时,需要进行 pay_show 上报
c#
ZMYSDKManager.I.ReportPayShow(string productId);
IAPButton(推荐使用)
为了方便接入,集成商品购买、上报、获取信息功能,封装成IAPButton
组件

示例
C#
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;
using ZMYSDK;
using UnityEngine;
using UnityEngine.UI;
public class IapTest : MonoBehaviour
{
#if UNITY_ANDROID
/// 金币-消耗型商品
private const string globKey = "com.test.test.test.pigbank1";
/// vip--非消耗型商品
private const string vipKey = "com.test.test.test.noad";
/// 月卡--订阅型商品
private const string monthlyCardKey = "test.test1";
#else
/// 金币-消耗型商品
private const string globKey = "com.test.test.test.pigbank";
/// vip--非消耗型商品
private const string vipKey = "com.test.test.test.noad";
/// 月卡--订阅型商品
private const string monthlyCardKey = "test.tset";
#endif
private int m_glob;
private bool m_vip;
private int m_mcState;
private string m_mcExpiresDate;
public Text m_glotxt;
public Toggle m_vipToggle;
public Text m_monthlyCardtxt;
public GameObject m_storePlan;
public GameObject m_loading;
public Text m_resultTxt;
public GameObject m_azStore;
public GameObject m_iosStore;
void Awake()
{
#if UNITY_ANDROID
m_azStore.SetActive(true);
m_iosStore.SetActive(false);
#else
m_azStore.SetActive(false);
m_iosStore.SetActive(true);
#endif
//读取之前缓存的值
m_glob = PlayerPrefs.GetInt(globKey,0);
m_vip = PlayerPrefs.GetInt(vipKey, 0) == 1;
m_mcState = PlayerPrefs.GetInt(monthlyCardKey,2);
m_mcExpiresDate = PlayerPrefs.GetString(monthlyCardKey+"Time");
ReshUI();
}
void ReshUI()
{
m_glotxt.text = "金币数量:"+m_glob.ToString();
m_vipToggle.isOn = m_vip;
//订阅商品只处理1 和2 其他情况维持默认
if (m_mcState == 1)
{
m_monthlyCardtxt.text = $"月卡用户,时间:[{m_mcExpiresDate}]";
}
else if (m_mcState == 2)
{
m_monthlyCardtxt.text = "订阅已过期";
}
}
// Start is called before the first frame update
void Start()
{
//使用内购前,要先初始化
ZMYSDKManager.I.InitIap(new List<ProductData>()
{
new ProductData(){productID = globKey,productType = ProductType.Consumable},//金币
new ProductData(){productID = vipKey,productType = ProductType.NoConsumable},//vip
new ProductData(){productID = monthlyCardKey,productType = ProductType.subscription},//月卡
});
//注册内购相关事件
ZMYSDKManager.I.Iap_ShowLoading += ShowLoading;
ZMYSDKManager.I.Iap_CloseLoading += CloseLoading;
ZMYSDKManager.I.Iap_ReceiveConsumableCommodity += ReceiveConsumableCommodity;
ZMYSDKManager.I.Iap_ReshNonConsumableState += ReshNonConsumable;
ZMYSDKManager.I.Iap_ReshSubscriptionState += ReshSubscription;
ZMYSDKManager.I.GetQryGivenGiftCallback += GetQryGivenGiftCallback;
//为了测试需要,延迟一会激励视频窗口的出现
StartCoroutine(wait());
m_loading.SetActive(false);
}
IEnumerator wait()
{
yield return new WaitForSeconds(1);
m_storePlan.SetActive(true);
}
public void Click_RestoreBuy()
{
ZMYSDKManager.I.RequestRestoreBuy();
}
public void Click_CheckSubscription()
{
ZMYSDKManager.I.RequestCheckSubscription(monthlyCardKey);
}
void ShowLoading()
{
m_loading.SetActive(true);
}
void CloseLoading()
{
m_loading.SetActive(false);
}
/// 消耗商品
/// <param name="channel"></param>
/// <param name="key"></param>
public void ReceiveConsumableCommodity(CommdityChannel channel, string key)
{
m_resultTxt.text = $"IAP:产品[{key}]购买成功,购买渠道[{channel}]";
m_glob++;
PlayerPrefs.SetInt(globKey, m_glob);
ReshUI();
}
/// 非消耗商品
/// <param name="channel"></param>
/// <param name="key"></param>
public void ReshNonConsumable(CommdityChannel channel, string key)
{
m_resultTxt.text = $"IAP:产品[{key}]购买成功,购买渠道[{channel}]";
if (key.Equals(vipKey))
{
m_vip = true;
PlayerPrefs.GetInt(vipKey, 1);
}
ReshUI();
}
/// 订阅商品
/// <param name="channel"></param>
/// <param name="key"></param>
public void ReshSubscription(NonConsumableState state)
{
m_resultTxt.text = $"IAP:刷新订阅产品[{state.productId}]状态,订阅状态[{state.state}] 时间:[{state.expiresDate}]";
m_mcState = state.state;
m_mcExpiresDate = state.expiresDate;
PlayerPrefs.SetInt(monthlyCardKey, 2);
PlayerPrefs.SetString(monthlyCardKey + "Time", m_mcExpiresDate);
ReshUI();
}
public void Click_QryGivenGift()
{
ZMYSDKManager.I.Sdk.GetQryGivenGiftStatic();
}
void GetQryGivenGiftCallback(List<QryGivenGift> datas)
{
for (int i = 0; i < datas.Count; i++)
{
QryGivenGift item = datas[i];
//模拟发货
m_glob++;
PlayerPrefs.SetInt(globKey, m_glob);
ReshUI();
string tips = $"收到服务器赠送道具:[{item.prodId} 数量:[{item.quantity}]]";
Debug.Log(tips);
m_resultTxt.text = tips;
//通知服务器发货成功
ZMYSDKManager.I.Sdk.UpSendGivenGiftResultStatic(item.orderNo);
}
}
void OnDestroy()
{
ZMYSDKManager.I.Iap_ShowLoading -= ShowLoading;
ZMYSDKManager.I.Iap_CloseLoading -= CloseLoading;
ZMYSDKManager.I.Iap_ReceiveConsumableCommodity -= ReceiveConsumableCommodity;
ZMYSDKManager.I.Iap_ReshNonConsumableState -= ReshNonConsumable;
ZMYSDKManager.I.Iap_ReshSubscriptionState -= ReshSubscription;
ZMYSDKManager.I.GetQryGivenGiftCallback -= GetQryGivenGiftCallback;
ZMYSDKManager.I.CloseIap();
}
}
核心接口使用 GameHelper.Pay
调用
- 在进入游戏后开始核查未完成订单,然后在相应的购买和恢复按钮中调用购买或者恢复接口
checkFailOrders
内部会轮询核查,游戏只需要在 SDK 初始化完成后调用该方法,返回的结果按照product.prodId
下发道具,完成后返回 true 即可
TypeScript
GameHelper.Pay.checkFailOrders((product) => {
If (product.prodId == "xxx.x.xx.xx") {
// 发送奖励
return true
} else if (product.prodId == "xxx.x.xx.xx") {
// 发送奖励
return true
}
})
是否支持内购
TypeScript
public isSupportPayStatic() : boolean {}
开始加载商品信息
注意 此方法只是开始加载 获取结果使用 getProductInfo 方法,可在首页调用一次就好
TypeScript
// 1.启动时查询想要查询的id,可以多个。(也可以启动时不查询,展示商品界面前查询,这里不做任何限制,游戏怎么方便怎么来。)
// 2.展示商品界面时获取某个id的详细信息,游戏自行显示界面数据。注:可能不一定成功,如果获取为空,那么游戏需要显示默认的。
loadProductInfo(productIds : Array<string>) {}
获取商品信息(查询动态价格)
注意 先使用 loadProductInfo 加载后才能获取,加载失败或未完成时 返回 空
此接口不宜调用太早,推荐是在即将展示商品时再调用,获取的是支付sdk初始化之后缓存到本地的数据,所以可以多次高频率调用。
TypeScript
// 获取产品信息 可能会返回null 需要在此之前(首页 或者其他页面)调用loadProductInfo 加载(loadProductInfo内部是一个异步请求)
getProductInfo(productId : string) : ProductInfo | null
购买
注意 success 的返回值 发送奖励如果成功了 返回 true 否则 false
TypeScript
/**
* 创建订单信息 只用商品id
* @param productID skuId
* @param ext额外信息(预留字段,暂不支持)
* @success 注意返回值是boolean 当 发送完奖励后 返回true 如果失败 返回false 发送奖励过程中失败也当作内购失败处理
*/
public buyProduct(obj : {productID : string, ext? : string,
success: (productID : string, orderId : string, quantity : number) => boolean,
fail?: (productId: string, orderId: string, msg : string) => void }) {}
伪代码
TypeScript
GameHelper.Pay.buyProduct({
productID : "xxxxxxxxxx",
success : (productId: string, orderId: string, quantity : number) => {
// 发放奖励
return true
}
})
恢复购买
WARNING
💡 含有非消耗型内购的项目,需要在设置面加上恢复购买
按钮
如果存在非消耗型商品携带了消耗道具的情况(如去广告礼包,赠送一定数量金币),请在发货接口里面判断,如果发货此商品来源是恢复购买,只发放去广告权益,不要发放消耗型道具。
TypeScript
//恢复购买 当用户购买当前产品后,用户卸载重装游戏或者换了一个手机,那么之前的购买必须依然有效,所以需要恢复购买的功能
//objc需要两个属性 productIds(需要恢复产品Id 数组) completion (恢复完成 (products : Array<{productId : string, orderId? : string}>) => bool)
//products里面有需要回复的产品 返回true
// 注意 此回调会恢复所有曾经内购的产品,并把成功的产品回调给你(产品id一定会有 订单id不一定有) 恢复完成
public restoreProducts(objc: { productIds: string[], completion: (products: Array<{productId : string, orderId? : string}>) => boolean}) {}
伪代码
TypeScript
GameHelper.Pay.restoreProducts({
productIds : ["xxxxxxxx", "xxxxxxaaaa"],
completion : (products: Array<{productId : string, orderId? : string}>) => {
//通过productId判断给哪个产品奖励
if (products.length > 0) {
if (productId == 'xxx.xxx.xxx') {
// 发送奖励
return true
} else if (productId == 'xxx.xxx.xxx') {
// 发送奖励
return true
}
}
}
})
非消耗商品的消耗包处理
由于非消耗商品只能购买一次,所以测试经常会没法测试非消耗商品的购买流程,为此我们需要打一个专门用来消耗非消耗商品的包,启动消耗后,再装正式包即可再次购买非消耗商品。
消耗包处理方式:
在模板工程的src/com/adv/core/AdsConstant.java文件中,添加以下代码
java
public final static boolean PayConsume = true;
测试流程:
再次购买非消耗商品,会有 PayModule-PayManagerGoogle-GooglePayUtil !!!!!开启非消耗商品消耗包模式!!!!!
样式日志打印
WARNING
💡 添加代码打包后,记得及时去除,不要将此配置带到正式包内。
查询订阅状态
我们购买的是订阅产品后 需要核查当前购买的产品是否到期
核查订阅需要用到产品 id 和 订单 id; 订单 id 是调用 buyProduct
后返回的,我们需要保存
TypeScript
/**
* 获取指定商品的订阅状态。订阅的结果完全依赖订阅查询接口,发起支付后需要查询订阅
* 如果是订阅产品 在发起内购后 要通过订阅接口来查询商品情况 再来发送奖励
* 不要频繁调用,最好非常低频率的使用,例如app仅仅只回到首页调用 所以游戏需要记录上次的购买时的产品id和订阅id 方便查询
* productId 产品ID orderId 平台订单ID ios专用
* 获取订阅结果回调
* @param productId 商品ID
* @param status 商品状态(0-未订阅 或者 查不到,需要根据上次保存的到期时间跟当前时间对比来判断是否是到期。目前平台处理了状态0,对0的订单会转换成1和2返回。,1-在订阅期内,2-已过订阅期, 3-服务端状态正在更新需要间隔1s再次重新查询)
* @param expiresDate 订阅到期时间 示例: 2021-07-08 15:26:26
*/
public checkSubscriptionStatus(productId : string, orderId : string, callback : (productId : string, status : number, expiresDate : string) => void) {}
伪代码
TypeScript
一般进入游戏后需要查询一次订阅是否到期,游戏记录下上次内购时的产品id 和订单id
GameHelper.Pay.buyProduct({
productId : "xxxxxxxxxx",
success : (productId: string, orderId: string, quantity : number) => {
GameHelper.Pay.checkSubscriptionStatus(productId, orderId, (productId : string, status : number, expiresDate : string) => {
if (status == 1) {
//发送奖励
}
})
return true
}
})
查询未完成订单
有的购买付了钱但是最终因为各种原因没有给到奖励,比如购买过程中退出游戏了。此时我们需要在进入游戏后核查未完成订单,重新给到奖励
TypeScript
/*
游戏启动后,SDK内部开始核查未完成订单,游戏补发奖励,补发完成后completion的返回值返回true
OrderInfo: {
...;
isAcknowledged // true 是自动触发的恢复购买 false 初次购买
order_type:1; // 0是未完成 1是复送 2服务器道具赠送
}
*/
public checkFailOrders(completion : (product : OrderInfo) => boolean) {}
伪代码
TypeScript
GameHelper.Pay.checkFailOrders((product) => {
If (product.prodId == "xxx.x.xx.xx") {
// 发送奖励
return true
} else if (product.prodId == "xxx.x.xx.xx") {
// 发送奖励
return true
}
})
点我快速对接


