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

这篇具有很好参考价值的文章主要介绍了苹果(apple)支付退款通知、api。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

背景:

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

方案选型:

  1. 主动查询退款订单,文档地址
  2. 被动接收服务器通知,文档地址
主动请求退款api:
  1. 生成jwt身份标识,文档地址

    • 添加依赖项

      <!-- jwt -->
      		<dependency>
      			<groupId>com.auth0</groupId>
      			<artifactId>java-jwt</artifactId>
      			<version>3.8.1</version>
      		</dependency>
      
    • 示例代码

       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")));
          }
      
          /**
           * 获取私钥
           * @param filename apple connect下载的p8文件路径
           * @return
           * @throws Exception
           */
          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{
              /**
               * 同一个用户的下的退款订单列表,jws格式
               */
              private List<String> signedTransactions;
      
              /**
               * 分页token
               * signedTransactions最大数量是20条,超过20条需要下一次请求带上分页token
            */
              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; // 商品数量
              /**
               *  <a href="https://developer.apple.com/documentation/appstoreserverapi/type/">消费类型</a>
               *  Consumable 消耗品
               */
              private String type;
              /**
               *  <a href="https://developer.apple.com/documentation/appstoreserverapi/type/">所有类型</a>
               * FAMILY_SHARED 交易属于受益于服务的家庭成员。
               * PURCHASED 交易属于买方。
               */
              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..."}
        
      • 验签并解析数据

        		/**
             * 验证签名
             * @param decodedJWT
             * @return
             * @throws CertificateException
             */ 		
        		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()));
        			
            }
        
        
        		/**
             * 获取公钥
             * @param x5c
             * @return
             * @throws CertificateException
             */
            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://www.toymoban.com/news/detail-467310.html

到了这里,关于苹果(apple)支付退款通知、api的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • SpringBoot 整合微信小程序微信支付V3 jsapi (支付、退款)

    最近的一个微信小程序项目里有用到微信支付,网上找的资料都是特别乱,看起来特别懵,结合了好多文章的内容,终于做了出来,可能我的这个博文看起来也是特别乱,但是是可以直接C走简单改一改就可以用的。(支付成功回调,和退款回调因为昨天刚在阿里申请的域名还

    2024年04月25日
    浏览(46)
  • 【微信支付】springboot-java接入微信支付-JSAPI支付/查单/退款/发送红包(二)---查单

    文章地址:https://blog.csdn.net/ssdadasd15623/article/details/134684556 查询订单分为微信订单号查询以及商户订单号查询,这里使用商户订单号,也就是自己的系统的订单号 https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/query-by-out-trade-no.html 在请求接口时,注意⚠️:请求参数内的Authori

    2024年02月03日
    浏览(43)
  • 微信小程序基于java实现v2支付,提现,退款

    v2微信官方文档 封装支付请求实体 controller接口暴露层 payFoodOrder 支付接口实现类 获取请求ip wxform.setNotifyUrl(WechatUtil.getPayNotifyUrl() + WXPAY_NOTIFY_URL_FOOD_ORDER); 这个回调地址是你自己代码里面定义的回调接口,例如你定义的controller回调接口url是 feedback/wx/notifurl , 即是 wxform.setNoti

    2024年02月09日
    浏览(52)
  • 微信小程序开发实战11_4 微信支付退款流程

    当交易发生之后一年内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付金额退还给买家,微信支付将收到退款请求并且验证成功之后,将支付款按原路退还至买家账号上。使用该接口时的一些注意事项如下: 交易时间超过一年的订单无法提交退款。 微

    2024年02月11日
    浏览(40)
  • java整合快手小程序(登陆,支付,结算,退款,手机号授权登陆)

    快手小程序官网地址 快手小程序后台配置回调域名 KSUrlConstants(请求地址常量) 商品类目编号 根据业务自行替换 RestTemplateUtil (rest发送请求工具类) KsUtil 支付前需要先获取到用户的openId,用户openId参与支付签名 支付前需要先获取到支付权限的access_token access_token 在支付 结算

    2024年02月11日
    浏览(46)
  • 蓝牙资讯|苹果Apple Watch可手势操控Mac和Apple TV等设备

    根据美国商标和专利局(USPTO)公示的清单,苹果公司近日获得了一项技术专利,概述了未来的 Apple Watch 手表,使用手势等操控 Mac 和 Apple TV 等设备。 该专利描述未来 Apple Watch 可以交互实现编辑图像、绘图、处理文档、制作电子表格、玩游戏、打电话、视频会议、电子邮件

    2024年02月13日
    浏览(35)
  • 【三方登录-Apple】iOS 苹果授权登录(sign in with Apple)之开发者配置一

    记录一下sign in with Apple的开发者配置 关于使用 Apple 登录 使用“通过 Apple 登录”可让用户设置帐户并使用其Apple ID登录您的应用程序和关联网站。首先使用“使用 Apple 登录”功能启用应用程序的App ID 。 如果您是首次启用应用程序 ID 或为新应用程序启用应用程序 ID,请启用该

    2024年02月06日
    浏览(70)
  • 苹果 Apple 发布的 AR 头显 Vision Pro 介绍

    苹果今天凌晨的发布会,隆重推出了用了8 年时间研发的AR(增强现实)头戴显示器 Vision Pro。作为苹果 AR系列的最新成员,为用户带来了前所未有的沉浸式增强现实体验。 硬件 12个摄像头 ,包括苹果首个 3D 相机,5个传感器,6个麦克风,实时将你身边的环境搬进虚拟空间,

    2024年02月03日
    浏览(42)
  • 需要买apple pencil吗?苹果平板触控笔推荐

    随着科技的进步,各种类型的电容笔相继问世。一支好的电容笔,不仅能大大提高我们的工作效率,而且能大大提高我们的学习效率。平替电容笔,无论从技术上,还是从产品品质上来看,都有很大的发展空间,书写上跟Apple Pencil的使用体验,并没有太大的区别。以下是我为

    2024年02月11日
    浏览(43)
  • 苹果平板电容笔好用吗?第三方apple pencil推荐

    自从苹果推出了ipad的电容笔之后,一直在市场上保持着十分火爆的热度,但是因为Apple Pencil的价格太高,一般的消费者根本没有足够预算去入手。所以市场上就不断涌现出了不少可以很好代替Apple Pencil的平替电容笔,并且深受人们的热爱。而且这种平替电容笔在书写体验上和

    2024年02月17日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包