返回顶部
首页 > 资讯 > 精选 >苹果(apple)支付退款通知、api
  • 352
分享到

苹果(apple)支付退款通知、api

java后端iosjson 2023-08-20 08:08:18 352人浏览 独家记忆
摘要

苹果(apple)支付退款通知、api 背景: 用户在使用苹果支付购买商品后,可以直接像苹果申请退款,如果申请成功将导致商户直接构成损失。甚至某网络平台有这种专门薅羊毛的店铺,低价出售虚拟商品,再申请退款。所以有必要对用户发起的退款订单做及

苹果(apple)支付退款通知、api

背景:

用户在使用苹果支付购买商品后,可以直接像苹果申请退款,如果申请成功将导致商户直接构成损失。甚至某网络平台有这种专门薅羊毛的店铺,低价出售虚拟商品,再申请退款。所以有必要对用户发起的退款订单做及时响应,比如扣除对应的虚拟商品或像apple官方提供凭证使其退款不成功。

方案选型:

  1. 主动查询退款订单,文档地址
  2. 被动接收服务器通知,文档地址

主动请求退款api:

  1. 生成Jwt身份标识,文档地址

    • 添加依赖项

      com.auth0java-jwt3.8.1
    • 示例代码

       private String generateJwtToken() throws Exception {        Map<String, Object> headers = new HashMap<>();        // apple指定ES256算法        headers.put("alg", "ES256");        // 密钥ID        headers.put("kid", "***********");        // jwt格式        headers.put("typ", "JWT");        return JWT.create()                .withHeader(headers)                // issId:见apple connect后台右上角                .withIssuer("*******-4f2e-4296-b2c7-**********")                // 签名日期                .withIssuedAt(new Date())                // 失效日期:最晚一个小时,否则报错401                .withExpiresAt(DateUtils.addHours(new Date(), 1))                // 目标接收者,固定值                .withAudience("appstoreconnect-v1")                // 包名,bundleId                .withClaim("bid", "com.********")                // 签名密钥,需要用到apple connect下载p8文件                .sign(AlGorithm.ECDSA256(null, (ECPrivateKey) getPrivateKey("/payment/apple/AuthKey_****.p8")));    }        private PrivateKey getPrivateKey(String filename) throws Exception {        String content = new String(Files.readAllBytes(Paths.get(filename)), StandardCharsets.UTF_8);        try {            String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "")                    .replace("-----END PRIVATE KEY-----", "")                    .replaceAll("\\s+", "");            KeyFactory kf = KeyFactory.getInstance("EC");            return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));        } catch (InvalidKeySpecException e) {            throw new RuntimeException("Invalid key fORMat");        }    }
  2. 请求、解析数据:

    • API地址:生产环境:GET https://api.storekit.itunes.apple.com/inApps/v2/refund/lookup/{originalTransactionId};沙盒环境:GET Https://api.storekit-sandbox.itunes.apple.com/inApps/v2/refund/lookup/{originalTransactionId}

    • 示例代码和返回值:

        private RefundHistResponseVO getRefundHist() throws Exception {        String token = generateToken();        HttpHeaders header = new HttpHeaders();        header.set("Authorization", "Bearer "+ token);        RequestEntity<Map<String, String>> requestEntity = new RequestEntity<>(header, HttpMethod.GET, URI.create("https://api.storekit-sandbox.itunes.apple.com/inApps/v2/refund/lookup/2000000308586738"));        ResponseEntity<RefundHistResponseVO> exchange = restTemplate.exchange(requestEntity, RefundHistResponseVO.class);        return exchange.getBody(); }@Data    public static class RefundHistResponseVO{                private List<String> signedTransactions;                private String revision;                 private Boolean hasMore;    }
      {    "signedTransactions": [       "eyJhbGCiOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQtFVRUN3D0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeE1EZ3lOVEF5TlRBek5Gb1hEVEl6TURreU5EQXlOVEF6TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNST...",        "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeE1EZ3lOVEF5TlRBek5Gb1hEVEl6TURreU5EQXlOVEF6TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU...",        "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeE1EZ3lOVEF5TlRBek5Gb1hEVEl6TURreU5EQXlOVEF6TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRm..."    ],    "revision": "1680777292000_2000000308641111",    "hasMore": false}
    • 附上jws格式解析代码和解析后的参数示例:

       public void handleRefundOrder() throws Exception {        RefundHistResponseVO refundHist = getRefundHist();        for (String signedTransaction : refundHist.getSignedTransactions()) {            DecodedJWT decode = JWT.decode(signedTransaction);            TransactionResponseVO responseVO = JSONObject.parseObject(new String(Base64.getDecoder().decode(decode.getPayload())), TransactionResponseVO.class);            // do something        }    }@Data    public static class TransactionResponseVO{        private String transactionId; // 交易订单号        private String originalTransactionId; // 原始交易订单号        private String bundleId; // 包名        private String productId; // 商品编号        private Date purchaseDate; // 购买日期        private Date originalPurchaseDate; // 原始订单购买日期        private Integer quantity; // 商品数量                private String type;                private String inAppOwnershipType;        private Date signedDate; // 签名日期        private Integer revocationReason; // 退款方 1: apple ;0:客户        private Date revocationDate; // 退款时间        private String environment; //环境    }
      {  "transactionId": "2000000308611111",  "originalTransactionId": "2000000308611111",  "bundleId": "com.******",  "productId": "a10001",  "purchaseDate": 1680773327000,  "originalPurchaseDate": 1680773327000,  "quantity": 1,  "type": "Consumable",  "inAppOwnershipType": "PURCHASED",  "signedDate": 1681198157817,  "revocationReason": 0,  "revocationDate": 1680777292000,  "environment": "Sandbox"}
    • 这里使用的是v2版本的api接口,有几个注意点:

      1. 返回每页大小是20,下一页请求需带上revision参数,v1版本接口是50条且明文显示,官方不建议使用,文档地址

      2. 此接口只能查询已成功的退款订单(在生产环境,用户发起退款会有12小时的审核时间,开发者可以提供凭证证明商品发放成功),不能查询已发起但未通过的退款申请。

      3. original_transaction_id是必传参数,可以是任意一笔交易id,重点是交易id所关联的用户:apple ID,也就是说同一个appleId产生的订单在这个接口返回的结果是一样的。(吐槽一下苹果的api。。这个接口不能查询app范围内的所有退款订单,因为订单id是必传字段,但返回值跟这笔订单又没有强关联,而是关联用户)

      4. 如果需要做邮件实时通知、用户发起退款申请后自动响应。建议使用另外一种方法:接收服务器通知

    接收服务器通知:

    1. 配置服务器通知url:文档地址,步骤挺简单的,打开应用配置,设置沙盒和生产环境的服务器接口地址即可。注意这里也选择V2版本通知,不建议使用V1版本。

    2. 通知类型:文档地址,这里主要关注几个消耗品商品购买的类型(其他类型包含订阅类型购买的变更通知有需要的可以自行对接):

      • CONSUMPTION_REQUEST:用户发起退款
      • REFUND:用户退款成功
      • TEST:通过api发起的测试通知,测试服务器的通知url是否配置成功
    3. 官方提供的沙盒环境测试退款方法:文档地址,需要在本地xcode跑StoreKit Test,挺麻烦的需要iOS开发人员支持。这里提供一个免费的WEBhook网址,可以用于本地测试接收通知:https://webhook.site/

    4. 用户发起退款申请后,服务器会收到一个notificationType = CONSUMPTION_REQUEST 的通知,代表用户发起退款,开发者可以在接收通知的逻辑中调用发送消费信息的api,以证明用户退款无效:文档地址

    5. 苹果审核退款申请通过后,服务器会收到一个notificationType = REFUND 的通知,代表退款成功,开发者可以在处理扣除虚拟商品等操作。下面是退款通知的数据示例及验证签名代码:

      • 请求报文:

        {"signedPayload":"eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeE1EZ3lOVEF5TlRBek5Gb1hEVEl6TURreU5EQXlOVEF6TTFvd2daSXhRREErQm..."}
      • 验签并解析数据

         private jsONObject verifyAndGet(String jws) throws CertificateException {    DecodedJWT decodedJWT = JWT.decode(jws);        // 拿到 header 中 x5c 数组中第一个        String header = new String(java.util.Base64.getDecoder().decode(decodedJWT.getHeader()));        String x5c = JSONObject.parseObject(header).getJSONArray("x5c").getString(0);        // 获取公钥        PublicKey publicKey = getPublicKeyByX5c(x5c);        // 验证 token        Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) publicKey, null);        try {            algorithm.verify(decodedJWT);        } catch (SignatureVerificationException e) {            return throw new RunTimeException();        }// 解析数据        JSONObject payload = JSONObject.parseObject(new String(java.util.Base64.getDecoder().decode(decodedJWT.getPayload()));    }    private PublicKey getPublicKeyByX5c(String x5c) throws CertificateException {        byte[] x5c0Bytes = java.util.Base64.getDecoder().decode(x5c);        CertificateFactory fact = CertificateFactory.getInstance("X.509");        X509Certificate cer = (X509Certificate) fact.generateCertificate(new ByteArrayInputStream(x5c0Bytes));        return cer.getPublicKey();    }
      • 解析的json示例

        {  "notificationType": "REFUND",  "notificationUUID": "334d1548-****-4ea9-****-e104731870b9",  "data": {    "appAppleId": 1617026651,    "bundleId": "com.*****",    "bundleVersion": "1",    "environment": "Sandbox",    "signedTransactionInfo": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeE1EZ3lOVEF5TlRBek5Gb1hEVEl6TURreU5EQXlOVEF6TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBi..."  },  "version": "2.0",  "signedDate": 1680778196476}
      • 需要注意的是这里的signedTransactionInfo依然是一个jws格式,且字段与主动查询的结果一致,用上面的代码和vo类再解码一次

         DecodedJWT decode = JWT.decode(signedTransactionInfo);            TransactionResponseVO responseVO = JSONObject.parseObject(new String(Base64.getDecoder().decode(decode.getPayload())), TransactionResponseVO.class);
      • 关于后置的业务处理就不过多赘述了,毕竟每个公司的业务不同,需要处理的逻辑也不同,没有参考价值。与产品沟通即可。

来源地址:https://blog.csdn.net/Arhhhhhhh/article/details/130082795

--结束END--

本文标题: 苹果(apple)支付退款通知、api

本文链接: https://lsjlt.com/news/376220.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作