侧边栏壁纸
  • 累计撰写 47 篇文章
  • 累计收到 0 条评论

App Store Server API 实践总结

2023-4-2 / 0 评论 / 213 阅读
温馨提示:
本文最后更新于 2023-4-2,已超过半年没有更新,若内容或图片失效,请留言反馈。

最近售后反馈一个问题,用户苹果支付了3笔订单 (截图也是3笔订单),其中一笔订单的虚拟币未到账。整个支付流程大概是这样的:

  1. 用户通过苹果支付后,客户端将苹果支付凭证传递给后端
  2. 后端通过支付凭证调用苹果服务的验签接口校验是否已支付
  3. 校验通过后将判断订单是否已支付,凭证是否唯一的(曾经未被校验过)
  4. 上报支付订单
  5. 充值虚拟币

我通过后台订单及日志发现实际上是2笔订单,那么还有一笔是啥情况?要么是用户支付完成客户端未调用支付接口,要么是用户支付完成后客户端没有拿到支付凭证。将这个问题反馈到客户端,客户端通过日志排查也未发现问题,通过苹果后台查看到账信息也不太可靠(不是实时的) 而且也不确定支付人的信息是否此人。于是想到截图中有个订单号(客户端告诉我这个是订单号的,其实我不知道 HAHAHA)

对于这种问题涉及到金额的问题,必须要给用户一个合理解释,哪怕是花上几个小时的时间也得搞清楚来龙去脉!

接下来我就得考虑通过这个订单看能不能调用苹果服务API获取到有效信息了。

翻阅了下 App Store Server API 发现了线索

查询用户订单的收据

# 使用订单ID从收据中获取用户的应用内购买项目收据信息
GET https://api.storekit.itunes.apple.com/inApps/v1/lookup/{orderId}

查询用户历史收据

# 获取用户在您的 app 的应用内购买交易历史记录
GET https://api.storekit.itunes.apple.com/inApps/v1/history/{originalTransactionId}

查询用户内购退款

# 获取 app 中为用户退款的所有应用内购买项目的列表
GET https://api.storekit.itunes.apple.com/inApps/v1/refund/lookup/{originalTransactionId}

查询用户订阅项目状态

# 获取您 app 中用户所有订阅的状态
GET https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{originalTransactionId}

提交防欺诈信息

# 当用户申请退款时,苹果通知(CONSUMPTION_REQUEST)开发者服务器,开发者可在12小时内,提供用户的信息(比如游戏金币是否已消费、用户充值过多少钱、退款过多少钱等),最后苹果收到这些信息,协助“退款决策系统” 来决定是否允许用户退款
PUT https://api.storekit.itunes.apple.com/inApps/v1/transactions/consumption/{originalTransactionId}

延长用户订阅的时长

# 使用原始交易标识符延长用户有效订阅的续订日期。(相当于免费给用户增加订阅时长)
PUT https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/extend/{originalTransactionId}

App Store Server API 是苹果提供给开发者,通过服务器来管理用户在 App Store 应用内购买的一套接口(REST API

线上环境的 URL

https://api.storekit.itunes.apple.com/

沙盒环境测试 URL

https://api.storekit-sandbox.itunes.apple.com/

在这里我们需要调用的是 查询用户订单的收据 接口,整个流程比较麻烦,需要鉴权、苹果后台获取密钥串、数据解密等,在这里就不一一表述了,当时是写了个 Python 拿到了具体的信息作对比,我直接贴生成密钥的方式和实现代码吧。

生成密钥 ID(kid)

kidiss 值是从 App Store Connect 后台创建和获取。

要生成密钥,您必须在 App Store Connect 中具有管理员角色或帐户持有人角色。登录 App Store Connect 并完成以下步骤:

  • 选择 “用户和访问”,然后选择 “密钥” 子标签页。
  • 在 “密钥类型” 下选择 “App内购买项目”。
  • 单击 “生成API内购买项目密钥”(如果之前创建过,则点击 “添加(+)” 按钮新增。)。
  • 输入密钥的名称。该名称仅供您参考,名字不作为密钥的一部分。
  • 单击 “生成”。

生成的密钥,有一列名为 “密钥 ID” 就是 kid 的值,鼠标移动到文字就会显示 拷贝密钥 ID,点击按钮就可以复制 kid 值。

生成 Issuer(iss)

同理,iss 值的生成,类似:

issuer ID 值就是 iss 的值。

生成和签名 JWT

获取到这里参数后,就需要签名,那么还需要签名的密钥文件。

下载并保存密钥文件

App Store Connect 密钥文件,在刚才生成 kid 时,列表右边有 下载 App 内购买项目密钥 按钮(仅当您尚未下载私钥时,才会显示下载链接)
此私钥只能一次性下载!Apple 不保留私钥的副本,将您的私钥存放在安全的地方。

注意:将您的私钥存放在安全的地方。不要共享密钥,不要将密钥存储在代码仓库中,不要将密钥放在客户端代码中。如果您怀疑私钥被盗,请立即在 App Store Connect 中撤销密钥。

API密钥有两个部分:苹果保留的公钥和您下载的私钥。开发者使用私钥对授权 API 在 App Store 中访问数据的令牌进行签名。

App Store Server API 密钥是 App Store Server API 所独有的,不能用于其他 Apple 服务(比如 Sign in with Apple 服务或 App Store Connet API 服务等)。

import base64
import json
import jwt
import requests
import time

from builtins import int, print
from io import open

# 读取密钥文件证书内容 (这个私钥证书在苹果后台获取, 上述获取密钥时有讲到)
f = open("/Users/linshan/Downloads/SubscriptionKey_Y8MPN22MHA.p8")
key_data = f.read()
f.close()

# JWT Header
header = {
    # App Store Server API 的所有 JWT 都必须使用 ES256 加密进行签名
    "alg": "ES256",   
    # 您的私钥ID,值来自 App Store Connect
    "kid": "Y8MPN22MHA",   
    "typ": "JWT"
}

# JWT Payload
payload = {
    # 您的发卡机构ID,值来自 App Store Connect 的密钥页面
    "iss": "6e043b68-462d-4e0a-a9a3-7c8d344af428",
    # 固定值
    "aud": "appstoreconnect-v1",
    # 秒,以 UNIX 时间(例如:1623085200)发布令牌的时间    
    "iat": int(time.time()),    
    # 秒,令牌的到期时间,以 UNIX 时间为单位。在iat中超过 60 分钟过期的令牌无效(例如:1623086400)
    "exp": int(time.time()) + 60 * 60,  
    # 您仅创建和使用一次的任意数字(例如: "6edffe66-b482-11eb-8529-0242ac130003")。可以理解为 UUID 值。
    "nonce": time.time(), 
    # 您的 app 的套装ID(例如:“com.example.testbundleid2021”), 可以向客户端提供。  
    "bid": "com.ls.stxl"    
}

# JWT token
token = jwt.encode(headers=header, payload=payload, key=key_data, algorithm="ES256")

print("JWT Token:", token)

# 请求链接和参数 (`MNHX98BS4X` 值为订单号)
url = "https://api.storekit.itunes.apple.com/inApps/v1/lookup/" + "MNHX98BS4X"
header = {
    "Authorization": f"Bearer {token}"
}
# 请求和响应
rs = requests.get(url, headers=header)
data = json.loads(rs.text)
print("Result:", data)
if data['status'] == 0:
    for item in data['signedTransactions']:
        item = item.split('.')
        item = base64.b64decode(item[1]+"=")
        value = json.loads(item)
        print("Order Info", value)

运行得到信息

具体里面的内容通过简单的分析就能得到,这里不再解释,主要就是根据用户订单支付时间作对比。最终发现是苹果订单支付回调给用户的问题(用户使用的是信用卡支付),即今天支付后有个明细,可能第二天才回调到用户的实际支付明细,给用户产生了被多次扣费的假象。

嗯,终于可以给售后答复了。。。

评论一下?

OωO
取消