跳到主要内容

订阅商品

前言

本篇文档介绍 游戏 方如何接入订阅商品生命周期通知。由于订阅商品的生命周期、支付逻辑与一次性购买商品相比区别都很大所以需要对订阅商品有一定地了解,然后才能更好的根据该文档接入 OmniSDK 订阅商品功能。

订阅介绍

  • 订阅是指用户可以在指定的时间段内享有某组特定权益,例如周卡、月卡、季卡、年卡,会员卡等。海外 OmniSDK 计划提供 Google Play Billing 订阅商品功能和 Apple Store IAP 订阅商品功能,目前只接入了 Google Play Billing 订阅商品功能。

  • Google Play Billing 订阅商品又可分为 自动续订非自动续订 两种类型(预付款类型),目前 OmniSDK 只接入了 自动续订 类型。 自动续订 商品的特点是首次购买时需要玩家在应用内选择商品并完成付款,以后每一期的续订购买不需要玩家动手付款,Google Play Store 会自动完成扣款。

  • Google Play Billing 订阅商品的生命周期和管理方式复杂多样,OmniSDK 目前只接入并实现了以下自动续订类型商品售卖功能。

    功能首期购买订阅免费试用自动续订订阅退款宽限期订阅过期取消自动续订恢复自动续订暂停自动续订价格变更订阅升降级变更订阅优惠延长订阅
    支持
  • 参考:

实现逻辑

概要

OmniSDK 根据 Gooogle Play BillingApple Store IAP 订阅商品的生命周期抽象出更为简单易懂地订阅生命周期方便 游戏 快速简单的接入订阅商品支付功能。 游戏 方需要关心的是:玩家是否购买订阅成功 玩家是否购买下一期订阅成功 玩家本期的订阅什么时间到期 玩家是否退款订阅了 ,而不需要关心背后的具体原因与流程。当用户完成付款/退款、订阅过期时间有变化、订阅过期时,OmniSDK 服务端会以 HTTPS POST 请求方式将相应的订阅消息以 application/json 格式的报文发送给 游戏 方。

FAQ

  1. OmniSDK 服务端什么时候会通知 游戏 订阅消息?

    • 订阅成功:用户首期购买订阅商品成功或者用户重新订阅了一个已经过期的订阅。
    • 续订成功:即 Google Play Store 完成了一次自动扣款,玩家应该拥有新一期的订阅权限。
    • 订阅进入宽限期:由于用户的付款方式存在问题导致 Google Play Store 不能完成自动扣款续订,或者用户主动暂停了自动续订,此时订阅会进入宽限期,宽限期一般为3天或者7天。 开发者可以在 Google Play Console 创建订阅商品时进行设置。需要注意的是订阅宽限期期间用于仍然享有订阅权益。
    • 订阅过期:当用户主动取消了自动续订或者用户的付款方式存在问题导致 Google Play Store 不能完成自动扣款时,最近一起的订阅到达过期时间时。
    • 订阅退款:订阅退款实际上是撤销,为了方便理解,OmniSDK 将其设计为了退款。当玩家申请退款成功以后或者开发者通过 API 调用 Google Play Developer API 的撤销接口成功以后,OmniSDK 会通知 游戏 订阅退款消息。 要注意的是该退款通知要区别于 应用内支退款通知
  2. 什么时候 OmniSDK游戏 方需要生成新的订单?

    用户的每一次付款成功 OmniSDK游戏 方都需要同时生成新的订单,即订阅成功续订订成功时。

订阅消息

  • Message
属性类型最大长度说明
messageIdstring32消息唯一ID,重复的订阅消息messageId不会变化
evenTypestring16事件类型:NEW-首期购买订阅成功;GRACE-订阅进入了宽限期阶段;RENEW-续订成功;EXPIRE-订阅过期;REFUND-订阅退款成功
eventTimeMillisstring13事件发生时间:毫秒级时间戳
signaturestring64对 Order 内容进行 HmacSHA256 的签名后得到的16进制字符串,用于消息通知的安全验证
subTypestring32事件子类型: OUT_OF_APP-应用外事件
versionstring16消息版本号,目前固定为 v1
orderjson-订单详情
  • EventType

    • NEW:首期购买订阅成功,Order 中的 expireTime 代表本期期结束时间。当 SubType 为空时证明订阅购买是从 游戏 内发起的,此时 游戏 方和 OmniSDK 方均已经生成过订单,此时不需要再生成新的订单。 当 SubTypeOUT_OF_APP 时,证明订阅是从应用外(Google Play Store)发起的,此时 游戏 方和 OmniSDK 都需要生成新的订单。 当 freeTrailtrue 时,证明本期订阅为免费试用,用户实际付款金额为0,expireTime 为免费试用到期时间。游戏 方需要授予玩家订阅权益。游戏 方 需要标记该笔订单的实际支付金额为0,以便于统计、对账。
    • GRACE:订阅进入宽限期,玩家让然应该享有订阅权益,Order 中的 expireTime 代表宽限期结束时间。
    • RENEW:续订成功,Order 中的 expireTime 代表宽限期结束时间。游戏 方和 OmniSDK 需要同时生成新的订单,并将上一期订阅订单标记为过期,避免用户享有同一订阅商品的多重权益。
    • EXPIRE:订阅过期,游戏 方需要停止用户对当前订阅的权益。
    • REFUND:退款,游戏 方需要停止用户对当前订阅的权益。同时可以根据用户已经享有订阅权益的时间酌情处理,例如收回部分道具。
  • SubType

    • :空子类型,根据 EventType 进行逻辑处理即可。
    • OUT_OF_APP:事件发生在应用外,目前只有一种情况,即用户订阅过期以后又在 Google Play Store 进行了重新订阅付款。
  • Signature

注意

订阅消息中的签名算法不同于 安全性 中的签名算法,但是使用相同的签名密钥。

  1. 假设 signKey 签名密钥为:f7d48ee9a2a44743847e4c110bdac234
  2. 通知消息的HTTP请求体为:
    {"evenType":"RENEW","eventTimeMillis":"1698650890673","messageId":"051dd7903d80f422256dba9d370be94c","order":{"appId":"1232","environment":"sandbox","expireTime":"1698651484104","gameOrderId":"","gameUserId":"123456","linkedGameOrderId":"mock-11ef10ac-6544-4633-8ba6-0b303d73bb51","linkedOrderId":"1718890029847326720","orderId":"1718892545502785536","originGameOrderId":"sdk_8b2ff1a67ba846e9848aabc31bffa26c","originOrderId":"1718887467567980544","productId":"com.sy.global.azhx.dytest05","provider":"google","providerOrderId":"GPA.3308-4634-1924-53758..1","state":"ACTIVE","userId":"1705162457208758272"},"signature":"b21c2819dd0739aa8ea4f57c899e16290d6b8b0236f5f62d9bef800ab7f312c7","subType":"","version":"v1"}
  3. 从请求体中提取出 order
    {"appId":"1232","environment":"sandbox","expireTime":"1698651484104","gameOrderId":"","gameUserId":"123456","linkedGameOrderId":"mock-11ef10ac-6544-4633-8ba6-0b303d73bb51","linkedOrderId":"1718890029847326720","orderId":"1718892545502785536","originGameOrderId":"sdk_8b2ff1a67ba846e9848aabc31bffa26c","originOrderId":"1718887467567980544","productId":"com.sy.global.azhx.dytest05","provider":"google","providerOrderId":"GPA.3308-4634-1924-53758..1","state":"ACTIVE","userId":"1705162457208758272"}
  4. 对提取出的order内容进行HmacSHA256签名计算,并将计算结果由字节转换为十六进制字符串,最终得到40位小写signature
    String signature = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, signKey).hmacHex(order); 
    // b21c2819dd0739aa8ea4f57c899e16290d6b8b0236f5f62d9bef800ab7f312c7
  • Order
属性类型最大长度说明
orderIdstring20OmniSDK 本期订阅订单号
originOrderIdstring20首期订阅 OmniSDK 订单号
linkedOrderIdstring20上一期订阅 OmniSDK 订单号
providerOrderIdstring64支付服务商本期订阅订单号(Goolge Play Billing 订单号 / Apple IAP 订单号)
gameOrderIdstring64游戏 本期订阅订单号
originGameOrderIdstring64游戏 首期订阅订单号
linkedGameOrderIdstring64游戏 上一期订阅订单号
appIdstring16OmniSDK 分配给 游戏 的应用 ID
providerstring16支付服务提供商:google、apple
userIdstring32OmniSDK 用户唯一ID,即登录OmniSDK以后获取到的 uid
gameUserIdstring64游戏 方唯一用户ID
productIdstring128商品ID
statestring16订阅订单状态:ACTIVE-活跃,GRACE-宽限期,EXPIRE-过期,REFUND-已退款
expireTimestring13订阅过期时间:毫秒级时间戳
environmentstring16订阅支付环境:sandbox-沙盒环境,production-正式环境
freeTrailstring4是否是免费试用:true-是,false-否
  • State
    • ACTIVE:活跃状态,用户应当享有订阅权益。
    • GRACE:宽限期,用户应当享有订阅权益。
    • EXPIRE:订阅过期,用户不应当享有订阅权益。
    • REFUND:用户已退款,用户不应当享有订阅权益。

响应通知

  • Response
属性类型说明
codeint响应状态码:0-成功,其它-失败
msgstring提示信息
orderjson游戏 方订单信息
  • Order
属性类型说明
gameOrderIdstring游戏 方产生的唯一订单号

重试策略

  1. 游戏 方接收到消息以后,返回 HTTP Status Code 200,并且 Response code 0 以后,OmniSDK 服务将视为消息处理成功不再进行重试通知。

  2. OmniSDK 服务器通知 游戏 服务器订阅消息的重试策略

    由于 OmniSDK 目前只接入了 Google Play Billing 订阅商品,Google Play Billing 订阅消息是通过 Google Cloud Pub/Sub 进行消息通知的。 Pub/Sub 有一套完善成熟的重试策略,所以 OmniSDK 直接依赖 Pub/Sub 的重试策略。重试策略为指数退避重试策略,重试的大概时间间隔为: 10s、20s、40s、80、160s、320s、600s、600s ... 最大重试间隔约为600s。重试总时间约为 7 天。

消息去重

  1. 可以根据 MessageId 对消息进行去重。
  2. 可以根据 OrderIdEventType 根据 游戏 自身内部逻辑对消息从逻辑上进行去重。例如同一个订单只可能发生一次购买成功事件;同一个订单 只可能发生一次退款成功事件;

幂等性

对于相同MessageId的消息,游戏 方处理成功以后再次接收到该消息总是应该回复 HTTP Status Code 200Response code 0 ,并且响应的 gameOrderId 不变。

接入流程

前置工作

订阅商品接入首先需要在 Google Play Console 和 OmniSDK Console 创建订阅商品,还需要在 Google Cloud Console 创建创建 Pub/Sub 主题、订阅, 并在 Google Play Console 配置创建好的 Pub/Sub 主题以确保当前 app 的订阅商品支付消息能够进入创建的 Pub/Sub 。该部分工作需要由运营人员或者开发人员提前完成。

Real-time developer notifications

  1. 游戏 方提供订阅商品信息,由运营人员配置到 OmniSDK ConsoleGoogle Play Console
  2. 游戏 方提供订阅消息通知接收地址,由运营人员或者 OmniSDK 人员配置到 OmniSDK Console
  3. OmniSDK 颁发 应用ID签名密钥游戏 方。注意:应用ID即接入 OmniSDK 时由 OmniSDK 分配的 应用ID签名密钥接收支付结果通知 中进行 安全验证secretKey 。但是签名验证不同于 接收支付结果通知 中的签名验证
  4. 接入订阅消息通知接口

消息通知

NEW

subType 为空时,证明是 游戏 内发起的首期订阅,游戏 需要验证该订单是否存在,商品信息、用户信息是否一致。此时的 orderId、originOrderId、linkedOrderId 相同均为本期订阅 OmniSDK 订单号;gameOrderId、originGameOrderId、linkedGameOrderId 也相同,均为应用内创建订单时 游戏 生成的唯一订单号。

subTypeOUT_OF_APP 时证明是用户在 Google Play Store 重新购买了已经过期的订阅,此时消息中的 gameOrderId 为空, 游戏 需要创建新的订单并返回 gameOrderId,消息中的 orderIdOmniSDK 生成的新订单号,originOrderId 为过期订阅的首期订单号,linkedOrderId 为过期订阅的最后一期订单号。消息中的 linkedGameOrderId、linkedGameOrderId 分别为已过期订阅的 首期 游戏 订单号和最后一期订单号。

freeTrailtrue 时,证明本期订阅为免费试用,用户实际付款金额为0,expireTime 为免费试用到期时间。游戏 方需要授予玩家订阅权益。游戏 方 需要标记该笔订单的实际支付金额为0,以便于统计、对账。

{
"evenType": "NEW",
"eventTimeMillis": "1705026799957",
"messageId": "499d3254d60d4362528d230488ba7b1c",
"order": {
"appId": "1232",
"environment": "sandbox",
"expireTime": "1705026973972",
"freeTrail": "true",
"gameOrderId": "sdk_79fd4d29b66244169f67c475697db1b1",
"gameUserId": "123456",
"linkedGameOrderId": "sdk_79fd4d29b66244169f67c475697db1b1",
"linkedOrderId": "1745635002785374208",
"orderId": "1745635002785374208",
"originGameOrderId": "sdk_79fd4d29b66244169f67c475697db1b1",
"originOrderId": "1745635002785374208",
"productId": "com.sy.global.azhx.dytest.free.01",
"provider": "google",
"providerOrderId": "GPA.3329-1838-7210-85440",
"state": "ACTIVE",
"userId": "1745268683472805888"
},
"signature": "aa6263add74400a219ebdc8448510e86bf129ea93d4c5bd4de1ee342e4f73a2d",
"subType": "EMPTY",
"version": "v1"
}

GRACE

收到该消息以后 游戏 需要更新本期订阅 expireTime 以延长用户享有订阅权益的时间。到达 expireTime 如果用户仍然没有续订成功 OmniSDK 会通知 游戏 EXPIRE 消息。如果到达 expireTime 之前完成了自动续订,则 OmniSDK 会通知 游戏 RENEW 消息。该消息中的 orderId 为本期订阅 OmniSDK 订单号,linkedOrderId 为上一期订阅 OmniSDK 订单号,originOrderId 为首期订阅订单号。gameOrderId、originGameOrderId、 originGameOrderId 分别为订阅的本期、首期、上一期 游戏 订单号。目前 GRACE 消息中的 subType 总是为空。

{
"evenType": "GRACE",
"eventTimeMillis": "1695445312962",
"messageId": "8f7a3151c66b7dda44a05aa359e5629c",
"order": {
"appId": "1232",
"environment": "sandbox",
"expireTime": "1695445605716",
"freeTrail": "false",
"gameOrderId": "sdk_03b8087e5d304e7988cd0b73fa2049ad",
"gameUserId": "123456",
"linkedGameOrderId": "sdk_03b8087e5d304e7988cd0b73fa2049ad",
"linkedOrderId": "1705444812846706688",
"orderId": "1705444812846706688",
"originGameOrderId": "sdk_03b8087e5d304e7988cd0b73fa2049ad",
"originOrderId": "1705444812846706688",
"productId": "com.sy.global.azhx.dytest06",
"provider": "google",
"providerOrderId": "GPA.3305-8989-7158-38098",
"state": "GRACE",
"userId": "1705162457208758272"
},
"signature": "b21c2819dd0739aa8ea4f57c899e16290d6b8b0236f5f62d9bef800ab7f312c7",
"subType": "",
"version": "v1"
}

RENEW

游戏 收到该消息以后需要创建新的订单,同时取消用户上一期订阅的权益,并返回 gameOrderId注意收到重复的消息通知请返回相同的 gameOrderId。 该消息中的 orderId 为新一期订阅的 OmniSDK 订单号,originOrderId、linkedOrderId 分别为首期订阅和上一期订阅 OmniSDK 订单号。该消息中的 originGameOrderId、linkedOrderId 分别为首期和上一期订阅 游戏 订单号。目前该消息中 subType 总是为空。

{
"evenType": "RENEW",
"eventTimeMillis": "1695445962138",
"messageId": "81f97ca0e5774c7a28cceb54e2f1d894",
"order": {
"appId": "1232",
"environment": "sandbox",
"expireTime": "1695446561669",
"freeTrail": "false",
"gameOrderId": "",
"gameUserId": "123456",
"linkedGameOrderId": "sdk_03b8087e5d304e7988cd0b73fa2049ad",
"linkedOrderId": "1705444812846706688",
"orderId": "1705450101566255104",
"originGameOrderId": "sdk_03b8087e5d304e7988cd0b73fa2049ad",
"originOrderId": "1705444812846706688",
"productId": "com.sy.global.azhx.dytest06",
"provider": "google",
"providerOrderId": "GPA.3305-8989-7158-38098..0",
"state": "ACTIVE",
"userId": "1705162457208758272"
},
"signature": "b21c2819dd0739aa8ea4f57c899e16290d6b8b0236f5f62d9bef800ab7f312c7",
"subType": "",
"version": "v1"
}

EXPIRE

游戏 收到该消息后需要立即停止用户订阅权益,该消息中的 orderId、originOrderId、linkedOrderId 分别为 OmniSDK 本期、首期、上一期订阅订单号。 该消息中的 gameOrderId、originGameOrderId、linkedGameOrderId 分别为 游戏 本期、首期、上一期订阅订单号。目前该消息中 subType 总是为空。

{
"evenType": "EXPIRE",
"eventTimeMillis": "1695446472996",
"messageId": "a1d7657706f33765cfc1565391383736",
"order": {
"appId": "1232",
"environment": "sandbox",
"expireTime": "1695446472021",
"freeTrail": "false",
"gameOrderId": "mock-fdb1c6ed-dd24-4c70-9090-af76e3d7ed29",
"gameUserId": "123456",
"linkedGameOrderId": "sdk_03b8087e5d304e7988cd0b73fa2049ad",
"linkedOrderId": "1705444812846706688",
"orderId": "1705450101566255104",
"originGameOrderId": "sdk_03b8087e5d304e7988cd0b73fa2049ad",
"originOrderId": "1705444812846706688",
"productId": "com.sy.global.azhx.dytest06",
"provider": "google",
"providerOrderId": "GPA.3305-8989-7158-38098..0",
"state": "EXPIRE",
"userId": "1705162457208758272"
},
"signature": "b21c2819dd0739aa8ea4f57c899e16290d6b8b0236f5f62d9bef800ab7f312c7",
"subType": "",
"version": "v1"
}

REFUND

订阅中的退款实际上是撤销(REVOKE),为了方便理解 OmniSDK 将其设计为退款处理。订阅撤销以后相应的会发生退款,游戏 接收到该消息以后应该立即停止用户订阅权益, 并自行处理扣除道具和其它用户封禁逻辑。目前该消息中 subType 总是为空。

{
"evenType": "REFUND",
"eventTimeMillis": "1695446470620",
"messageId": "e54b6d7808195d557dcb8217b0c8eb86",
"order": {
"appId": "1232",
"environment": "sandbox",
"expireTime": "1695446472021",
"freeTrail": "false",
"gameOrderId": "mock-fdb1c6ed-dd24-4c70-9090-af76e3d7ed29",
"gameUserId": "123456",
"linkedGameOrderId": "sdk_03b8087e5d304e7988cd0b73fa2049ad",
"linkedOrderId": "1705444812846706688",
"orderId": "1705450101566255104",
"originGameOrderId": "sdk_03b8087e5d304e7988cd0b73fa2049ad",
"originOrderId": "1705444812846706688",
"productId": "com.sy.global.azhx.dytest06",
"provider": "google",
"providerOrderId": "GPA.3305-8989-7158-38098..0",
"state": "REFUND",
"userId": "1705162457208758272"
},
"signature": "b21c2819dd0739aa8ea4f57c899e16290d6b8b0236f5f62d9bef800ab7f312c7",
"subType": "",
"version": "v1"
}