苹果(apple)支付退款通知、api 背景: 用户在使用苹果支付购买商品后,可以直接像苹果申请退款,如果申请成功将导致商户直接构成损失。甚至某网络平台有这种专门薅羊毛的店铺,低价出售虚拟商品,再申请退款。所以有必要对用户发起的退款订单做及
用户在使用苹果支付购买商品后,可以直接像苹果申请退款,如果申请成功将导致商户直接构成损失。甚至某网络平台有这种专门薅羊毛的店铺,低价出售虚拟商品,再申请退款。所以有必要对用户发起的退款订单做及时响应,比如扣除对应的虚拟商品或像apple官方提供凭证使其退款不成功。
添加依赖项
com.auth0 java-jwt 3.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"); } }
请求、解析数据:
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接口,有几个注意点:
返回每页大小是20,下一页请求需带上revision参数,v1版本接口是50条且明文显示,官方不建议使用,文档地址
此接口只能查询已成功的退款订单(在生产环境,用户发起退款会有12小时的审核时间,开发者可以提供凭证证明商品发放成功),不能查询已发起但未通过的退款申请。
original_transaction_id是必传参数,可以是任意一笔交易id,重点是交易id所关联的用户:apple ID,也就是说同一个appleId产生的订单在这个接口返回的结果是一样的。(吐槽一下苹果的api。。这个接口不能查询app范围内的所有退款订单,因为订单id是必传字段,但返回值跟这笔订单又没有强关联,而是关联用户)
如果需要做邮件实时通知、用户发起退款申请后自动响应。建议使用另外一种方法:接收服务器通知
配置服务器通知url:文档地址,步骤挺简单的,打开应用配置,设置沙盒和生产环境的服务器接口地址即可。注意这里也选择V2版本通知,不建议使用V1版本。
通知类型:文档地址,这里主要关注几个消耗品商品购买的类型(其他类型包含订阅类型购买的变更通知有需要的可以自行对接):
官方提供的沙盒环境测试退款方法:文档地址,需要在本地xcode跑StoreKit Test,挺麻烦的需要iOS开发人员支持。这里提供一个免费的WEBhook网址,可以用于本地测试接收通知:https://webhook.site/
用户发起退款申请后,服务器会收到一个notificationType = CONSUMPTION_REQUEST 的通知,代表用户发起退款,开发者可以在接收通知的逻辑中调用发送消费信息的api,以证明用户退款无效:文档地址
苹果审核退款申请通过后,服务器会收到一个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
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
2024-05-24
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0