文档管理中心

权益发放

对生效中的订阅发放权益

场景介绍

用户购买自动续期订阅商品后,若订阅处于生效状态,开发者需要及时给用户发放对应权益。

在应用启动时,获取用户当前处于生效状态的订阅列表,处理此部分订阅的权益发放。建议先检查当前订阅对应权益的发放状态,未发放再补充发放权益。在权益发放成功后,向IAP确认发货,完成购买。

建议单机应用将用户权益和订阅状态关联。如果订阅处于生效状态,始终为用户发放权益。

业务流程

  1. 应用客户端向IAP Kit发起queryPurchases请求,查询用户生效中的订阅列表。

  2. IAP Kit返回PurchaseData列表。PurchaseData为JWS格式的字符串,承载了相关的订阅信息。

  3. 应用客户端向应用服务器上报PurchaseData列表。

  4. 应用服务器需对每个PurchaseData.jwsSubscriptionStatus进行解码验签,验证成功可得到对应的SubGroupStatusPayload的JSON字符串。

  5. 处理权益发放。检查SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder是否已发放权益,未发放则需发放相关权益,并记录对应的订单信息(PurchaseOrderPayload)。

    说明

    建议单机应用将用户权益和订阅状态关联。如果订阅处于生效状态,始终为用户发放权益。

  6. 应用客户端向应用服务器查询订单的发货状态。

  7. 应用服务器返回对应的发货状态以及订单信息(PurchaseOrderPayload)。

  8. 发放权益后应用客户端向IAP Kit发送finishPurchase请求,以此通知IAP服务器更新商品的发货状态,完成购买流程。应用成功执行finishPurchase之后,IAP服务器会将相应商品标记为已发货状态。此步骤也可放到应用服务器处理。应用服务器可通过请求服务端订阅确认发货接口来确认发货,完成购买流程。

    说明

    对于自动续期订阅商品,如果不执行此步骤,会导致后续自动续期无法扣费 ,以及同一个订阅组不同自动续期订阅商品无法切换等问题。

开发步骤

  1. 应用客户端向IAP Kit发起queryPurchases请求,获取生效中的订阅列表。

    在请求参数QueryPurchasesParameter中指定productType为iap.ProductType.AUTORENEWABLE,同时指定queryType为iap.PurchaseQueryType.CURRENT_ENTITLEMENT。当接口请求成功时,IAP Kit将返回一个QueryPurchaseResult对象,该对象包含承载了订阅信息的PurchaseData的列表。

  2. 验证订单信息。对每个purchaseData.jwsSubscriptionStatus进行解码验签,验证成功可得到SubGroupStatusPayload的JSON字符串。建议应用客户端将purchaseData发送至应用服务器,在应用服务器执行此操作。

    为了提高安全性,可从SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder中解析出purchaseToken和purchaseOrderId信息,并通过服务端订阅状态查询接口向IAP服务器查询最新的订阅状态信息,进一步确认订阅信息的准确性。

  3. 展示订阅状态。

    • 如果SubGroupStatusPayload.lastSubscriptionStatus.status=1,表示订阅处于生效状态。
    • 如果SubGroupStatusPayload.lastSubscriptionStatus.status=1且SubGroupStatusPayload.lastSubscriptionStatus.renewalInfo.autoRenewStatusCode值为1时,表示订阅处于自动续期状态。此状态的商品无法再次购买,需要屏蔽相关的购买入口。
  4. 权益发放。获取SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder(下文标记为PurchaseOrderPayload),处理权益发放。

    可先检查此笔订单权益的发放状态,未发放则补充发放权益,成功后记录PurchaseOrderPayload等信息,用于后续检查权益发放状态。

    说明

    建议单机应用将用户权益和订阅状态关联。如果订阅处于生效状态,始终为用户发放权益。

  5. 在发放权益后,如果PurchaseOrderPayload.finishStatus不为1,应用需调用finishPurchase接口确认发货,完成购买流程。

    发起请求时,需在请求参数FinishPurchaseParameter中携带PurchaseOrderPayload中的productType、purchaseToken、purchaseOrderId。请求成功后,IAP服务器会将相应商品标记为已发货。

    说明

    此步骤也可放到应用服务器处理。应用服务器可通过请求服务端订阅确认发货接口来确认发货,完成购买流程。

    说明

    JWSUtil为自定义类,可参见示例代码

收起
自动换行
深色代码主题
复制
  1. import { iap } from '@kit.IAPKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. import { common } from '@kit.AbilityKit';
  4. import Logger from '../common/Logger';
  5. import { JWSUtil } from '../common/JWSUtil';
  6. import {
  7. FinishStatus,
  8. PurchaseData,
  9. PurchaseOrderPayload,
  10. SubGroupStatusPayload,
  11. SubStatus,
  12. } from '../common/IapDataModel';
  13. // ...
  14. await this.queryPurchases(iap.PurchaseQueryType.CURRENT_ENTITLEMENT);
  15. // ...
  16. queryPurchases(queryType: iap.PurchaseQueryType): Promise<void> {
  17. return new Promise<void>((resolve) => {
  18. const param: iap.QueryPurchasesParameter = {
  19. productType: iap.ProductType.AUTORENEWABLE,
  20. queryType: queryType,
  21. };
  22. iap.queryPurchases(this.context, param).then((res: iap.QueryPurchaseResult) => {
  23. Logger.info(TAG, 'Succeeded in querying purchases.');
  24. const purchaseDataList: string[] = res.purchaseDataList;
  25. if (purchaseDataList === undefined || purchaseDataList.length <= 0) {
  26. Logger.info(TAG, 'queryPurchases, purchaseDataList empty');
  27. resolve();
  28. return;
  29. }
  30. for (let i = 0; i < purchaseDataList.length; i++) {
  31. this.dealPurchaseData(purchaseDataList[i]);
  32. }
  33. resolve();
  34. }).catch((err: BusinessError) => {
  35. Logger.error(TAG, `Failed to query purchases. Code is ${err.code}, message is ${err.message}`);
  36. resolve();
  37. }).finally(() => {
  38. this.showNormalPage();
  39. });
  40. });
  41. }
  42. dealPurchaseData(purchaseData: string) {
  43. try {
  44. // 建议您将 purchaseData 发送到应用服务器进行签名验证。
  45. const jwsSubscriptionStatus = (JSON.parse(purchaseData) as PurchaseData).jwsSubscriptionStatus;
  46. if (!jwsSubscriptionStatus) {
  47. Logger.error(TAG, 'dealPurchaseData, jwsSubscriptionStatus invalid');
  48. return;
  49. }
  50. // 解码 jwsPurchaseOrder 并执行签名验证。
  51. const subscriptionStatus = JWSUtil.decodeJwsObj(jwsSubscriptionStatus);
  52. if (!subscriptionStatus) {
  53. Logger.error(TAG, 'dealPurchaseData, subscriptionStatus invalid');
  54. return;
  55. }
  56. // 需自定义SubGroupStatusPayload类,包含的信息请参见SubGroupStatusPayload
  57. const subGroupStatusPayload = JSON.parse(subscriptionStatus) as SubGroupStatusPayload;
  58. const lastSubscriptionStatus = subGroupStatusPayload.lastSubscriptionStatus;
  59. if (!lastSubscriptionStatus) {
  60. Logger.error(TAG, 'dealPurchaseData, lastSubscriptionStatus is invalid');
  61. return;
  62. }
  63. if (lastSubscriptionStatus.status === SubStatus.ACTIVE) {
  64. // 订阅已生效,您需要发货。
  65. const productId = lastSubscriptionStatus.renewalInfo?.productId;
  66. if (productId) {
  67. this.setProductInfoStatus(subGroupStatusPayload.subGroupId, productId, lastSubscriptionStatus.status);
  68. }
  69. // 在执行以下步骤之前,请确保发货成功。
  70. }
  71. const purchaseOrderPayload = lastSubscriptionStatus.lastPurchaseOrder;
  72. if (purchaseOrderPayload && purchaseOrderPayload.finishStatus !== FinishStatus.FINISHED) {
  73. // 向IAP Kit发送finishPurchase请求,以确认商品已发货并完成购买。
  74. this.finishPurchase(purchaseOrderPayload);
  75. }
  76. } catch (e) {
  77. Logger.error(TAG, 'dealPurchaseData json error');
  78. }
  79. }
  80. finishPurchase(purchaseOrder: PurchaseOrderPayload) {
  81. if (!purchaseOrder.productType) {
  82. Logger.error(TAG, 'finishPurchase but productType is empty');
  83. return;
  84. }
  85. const finishPurchaseParam: iap.FinishPurchaseParameter = {
  86. productType: Number(purchaseOrder.productType),
  87. purchaseToken: purchaseOrder.purchaseToken,
  88. purchaseOrderId: purchaseOrder.purchaseOrderId,
  89. };
  90. iap.finishPurchase(this.context, finishPurchaseParam).then(() => {
  91. Logger.info(TAG, 'Succeeded in finishing purchase.');
  92. }).catch((err: BusinessError) => {
  93. Logger.error(TAG, `Failed to finish purchase. Code is ${err.code}, message is ${err.message}`);
  94. });
  95. }

确保权益发放

用户购买自动续期订阅成功或者自动续期成功后,开发者需要及时给用户发放相关权益。但实际应用场景中,若出现异常(网络错误等)将导致应用无法知道用户实际是否支付成功,从而无法及时发放权益,即出现掉单情况。

为了确保权益发放,需要在createPurchase请求返回iap.IAPErrorCode.PRODUCT_OWNEDiap.IAPErrorCode.SYSTEM_ERROR时检查用户是否存在已购但未确认发货的商品,如果存在则发放相关权益,然后向IAP Kit确认发货,完成购买。

业务流程

  1. 应用客户端向IAP Kit发起queryPurchases请求,查询用户已购买但未确认发货的订阅列表。

  2. IAP Kit返回PurchaseData列表。PurchaseData为JWS格式的字符串,承载了相关的订阅信息。

  3. 应用客户端向应用服务器上报PurchaseData列表。

  4. 应用服务器需对每个PurchaseData.jwsSubscriptionStatus进行解码验签,验证成功可得到对应的SubGroupStatusPayload的JSON字符串。

  5. 处理权益发放。检查SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder是否已发放权益,未发放则需发放相关权益,并记录对应的订单信息(PurchaseOrderPayload)。

    说明

    建议单机应用将用户权益和订阅状态关联。如果订阅处于生效状态,始终为用户发放权益。

  6. 应用客户端向应用服务器查询订单的发货状态。

  7. 应用服务器返回对应的发货状态以及订单信息(PurchaseOrderPayload)。

  8. 发放权益后应用客户端向IAP Kit发送finishPurchase请求,以此通知IAP服务器更新商品的发货状态,完成购买流程。应用成功执行finishPurchase之后,IAP服务器会将相应商品标记为已发货状态。此步骤也可放到应用服务器处理。应用服务器可通过请求服务端订阅确认发货接口来确认发货,完成购买流程。

    说明

    对于自动续期订阅商品,如果不执行此步骤,会导致后续自动续期无法扣费 ,以及同一个订阅组不同自动续期订阅商品无法切换等问题。

开发步骤

  1. 应用客户端向IAP Kit发起queryPurchases请求,获取用户已购但未确认发货的订阅列表。

    在请求参数QueryPurchasesParameter中指定productType为iap.ProductType.AUTORENEWABLE,同时指定queryType为iap.PurchaseQueryType.UNFINISHED。当接口请求成功时,IAP Kit将返回一个QueryPurchaseResult对象,该对象包含承载了订阅信息的PurchaseData的列表。

  2. 验证订单信息。对每个purchaseData.jwsSubscriptionStatus进行解码验签,验证成功可得到SubGroupStatusPayload的JSON字符串。建议应用客户端将purchaseData发送至应用服务器,在应用服务器执行此操作。

    为了提高安全性,可从SubGroupStatusPayload.lastSubscriptionStatus.lastPurchaseOrder中解析出purchaseToken和purchaseOrderId信息,并通过服务端订阅状态查询接口向IAP服务器查询最新的订阅状态信息,进一步确认订阅信息的准确性。

  3. 处理权益发放。

    如果SubGroupStatusPayload.lastSubscriptionStatus.status=1,表示订阅处于生效状态。需要对生效状态的订阅处理权益发放。建议先检查此笔订单权益的发放状态,未发放则补充发放权益,成功后记录PurchaseOrderPayload等信息,用于后续检查权益发放状态。

    建议单机应用将用户权益和订阅状态关联。如果订阅处于生效状态,始终为用户发放权益。

  4. 在发放权益后,如果PurchaseOrderPayload.finishStatus不为1,应用需调用finishPurchase接口确认发货,完成购买流程。

    发起请求时,需在请求参数FinishPurchaseParameter中携带PurchaseOrderPayload中的productType、purchaseToken、purchaseOrderId。请求成功后,IAP服务器会将相应商品标记为已发货。

    说明

    此步骤也可放到应用服务器处理。应用服务器可通过请求服务端订阅确认发货接口来确认发货,完成购买流程。

    说明

    JWSUtil为自定义类,可参见示例代码

收起
自动换行
深色代码主题
复制
  1. import { iap } from '@kit.IAPKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. import { common } from '@kit.AbilityKit';
  4. import Logger from '../common/Logger';
  5. import { JWSUtil } from '../common/JWSUtil';
  6. import {
  7. FinishStatus,
  8. PurchaseData,
  9. PurchaseOrderPayload,
  10. SubGroupStatusPayload,
  11. SubStatus,
  12. } from '../common/IapDataModel';
  13. // ...
  14. queryPurchases(queryType: iap.PurchaseQueryType): Promise<void> {
  15. return new Promise<void>((resolve) => {
  16. const param: iap.QueryPurchasesParameter = {
  17. productType: iap.ProductType.AUTORENEWABLE,
  18. queryType: queryType,
  19. };
  20. iap.queryPurchases(this.context, param).then((res: iap.QueryPurchaseResult) => {
  21. Logger.info(TAG, 'Succeeded in querying purchases.');
  22. const purchaseDataList: string[] = res.purchaseDataList;
  23. if (purchaseDataList === undefined || purchaseDataList.length <= 0) {
  24. Logger.info(TAG, 'queryPurchases, purchaseDataList empty');
  25. resolve();
  26. return;
  27. }
  28. for (let i = 0; i < purchaseDataList.length; i++) {
  29. this.dealPurchaseData(purchaseDataList[i]);
  30. }
  31. resolve();
  32. }).catch((err: BusinessError) => {
  33. Logger.error(TAG, `Failed to query purchases. Code is ${err.code}, message is ${err.message}`);
  34. resolve();
  35. }).finally(() => {
  36. this.showNormalPage();
  37. });
  38. });
  39. }
  40. dealPurchaseData(purchaseData: string) {
  41. try {
  42. // 建议您将 purchaseData 发送到应用服务器进行签名验证。
  43. const jwsSubscriptionStatus = (JSON.parse(purchaseData) as PurchaseData).jwsSubscriptionStatus;
  44. if (!jwsSubscriptionStatus) {
  45. Logger.error(TAG, 'dealPurchaseData, jwsSubscriptionStatus invalid');
  46. return;
  47. }
  48. // 解码 jwsPurchaseOrder 并执行签名验证。
  49. const subscriptionStatus = JWSUtil.decodeJwsObj(jwsSubscriptionStatus);
  50. if (!subscriptionStatus) {
  51. Logger.error(TAG, 'dealPurchaseData, subscriptionStatus invalid');
  52. return;
  53. }
  54. // 需自定义SubGroupStatusPayload类,包含的信息请参见SubGroupStatusPayload
  55. const subGroupStatusPayload = JSON.parse(subscriptionStatus) as SubGroupStatusPayload;
  56. const lastSubscriptionStatus = subGroupStatusPayload.lastSubscriptionStatus;
  57. if (!lastSubscriptionStatus) {
  58. Logger.error(TAG, 'dealPurchaseData, lastSubscriptionStatus is invalid');
  59. return;
  60. }
  61. if (lastSubscriptionStatus.status === SubStatus.ACTIVE) {
  62. // 订阅已生效,您需要发货。
  63. const productId = lastSubscriptionStatus.renewalInfo?.productId;
  64. if (productId) {
  65. this.setProductInfoStatus(subGroupStatusPayload.subGroupId, productId, lastSubscriptionStatus.status);
  66. }
  67. // 在执行以下步骤之前,请确保发货成功。
  68. }
  69. const purchaseOrderPayload = lastSubscriptionStatus.lastPurchaseOrder;
  70. if (purchaseOrderPayload && purchaseOrderPayload.finishStatus !== FinishStatus.FINISHED) {
  71. // 向IAP Kit发送finishPurchase请求,以确认商品已发货并完成购买。
  72. this.finishPurchase(purchaseOrderPayload);
  73. }
  74. } catch (e) {
  75. Logger.error(TAG, 'dealPurchaseData json error');
  76. }
  77. }
  78. finishPurchase(purchaseOrder: PurchaseOrderPayload) {
  79. if (!purchaseOrder.productType) {
  80. Logger.error(TAG, 'finishPurchase but productType is empty');
  81. return;
  82. }
  83. const finishPurchaseParam: iap.FinishPurchaseParameter = {
  84. productType: Number(purchaseOrder.productType),
  85. purchaseToken: purchaseOrder.purchaseToken,
  86. purchaseOrderId: purchaseOrder.purchaseOrderId,
  87. };
  88. iap.finishPurchase(this.context, finishPurchaseParam).then(() => {
  89. Logger.info(TAG, 'Succeeded in finishing purchase.');
  90. }).catch((err: BusinessError) => {
  91. Logger.error(TAG, `Failed to finish purchase. Code is ${err.code}, message is ${err.message}`);
  92. });
  93. }
  94. // ...
  95. this.queryPurchases(iap.PurchaseQueryType.UNFINISHED);
在 指南 中进行搜索
请输入您想要搜索的关键词