对接苹果支付退款退单接口

前言

        一般而言,我们其实很少对接退款接口,因为退款基本都是商家自己决定后进行操作的,但是苹果比较特殊,用户可以直接向苹果发起退款请求,苹果觉得合理会退给用户,但是目前公司业务还是需要对接这个接口,可能是以后为了对账之类使用的吧

        本来对接api也没啥好说的,但是由于苹果官方是英文的,考虑到大部分人可能还是懒得找英文文档,所以进行了整理归档(我自己也是百度整理的...)

        以下为参考的一些地址,2023-11-22记录,目前是有限的,以后不确定..请知悉

参考对接地址: ​​​​​​苹果(apple)支付退款通知、api_苹果支付api_Arhhhhhhh的博客-CSDN博客

官网地址:

官网对接地址

主动通知地址:Get Refund History | Apple Developer Documentation

被动通知地址:Handling refund notifications | Apple Developer Documentation

必知

        这里主要介绍被动接收的(连接需要支持https),因为这种不是很好性能,主要是由于主动查询没有条件可以终止,所以选择用被动的,但是也会把相应工具类放上来,方便使用

对接步骤

配置通知URL

在 App Store Connect 进行配置,地址为:https://appstoreconnect.apple.com/login,由于我没有账号,所以是别人帮忙配的,如果不知道在哪配置可以参考这篇文章

苹果iOS内购三步曲:App内退款、历史订单查询、绑定用户防掉单!--- WWDC21 - 掘金

             我这里使用的是V2版本的,V1是明文的,不太安全,所以我这里采用了V2版本

引入依赖

        加解密需要引入工具包进行处理,以下是maven的坐标

<!-- jwt -->
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.8.1</version>
</dependency>

编写工具类

        这一步最重要,这里直接放代码,到时你们可以直接复制使用

主动调用工具类

public class AppStoreReturnUtil {//退款api正式环境private static final String APP_STORE_RETURN = "https://api.storekit.itunes.apple.com/inApps/v2/refund/lookup/{originalTransactionId}";//退款api沙箱环境private static final String APP_STORE_SANDBOX_RETURN = "https://api.storekit-sandbox.itunes.apple.com/inApps/v2/refund/lookup/{originalTransactionId}";/*** 生成token* @return* @throws Exception*/private static String generateJwtToken() throws Exception {Map<String, Object> headers = new HashMap<>();// apple指定ES256算法headers.put("alg", "ES256");// 密钥IDheaders.put("kid", "你的kid");// jwt格式headers.put("typ", "JWT");return JWT.create().withHeader(headers)// issId:见apple connect后台右上角.withIssuer("你的issId")// 签名日期.withIssuedAt(new Date())// 失效日期:最晚一个小时,否则报错401.withExpiresAt(DateUtils.addHours(new Date(), 1))// 目标接收者,固定值.withAudience("appstoreconnect-v1")// 包名,bundleId.withClaim("bid", "你的bundleId")// 签名密钥,需要用到apple connect下载p8文件.sign(Algorithm.ECDSA256(null, (ECPrivateKey) getPrivateKey("p8文件路径")));}/*** 获取私钥* @param fileName apple connect下载的p8文件路径* @return* @throws Exception*/private static 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");}}//任何http请求工具类都可以  private static 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();}

这里有几个注意的点,如下

1. getRefundHist 需要基于http工具去发送请求,你可以自己找你们项目中的,或者自己写一个

2. kid、issId、bundleId、p8文件都是你自己账号的,如果你不知道可以问ios或者产品经理要

3. originalTransactionId就是你之前下单时苹果返回的,所以这个数据你们之前必须要有

到此为止,剩下的就是你自己写代码去请求就行了

被动接收

苹果返回数据格式

格式如下(真实的很长,这里是为了你能看懂才故意弄短)

{"signedPayload":"BaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBh"}

如果你是用Java SpringBoot开发的话,可以直接这样接收(也就是用@RequestBody即可)

@RestController
@RequestMapping("app/store")
@Slf4j
public class AppStoreMsgController {@PostMapping("/notify")public String appStoreMsgNotify(@RequestBody AppStoreNotifyPayLoadDto appStoreNotifyPayLoadDto) {log.info("appStoreNotifyPayLoadDto{}", JsonUtils.Object2Json(appStoreNotifyPayLoadDto));return MSG.SUCCESS(result);}
}
@Data
public class AppStoreNotifyPayLoadDto implements Serializable {private static final long serialVersionUID = 1L;private String signedPayload;
}
 被动接收工具类
@Slf4j
public class AppStoreReturnUtil {/*** 验证签名并返回解析数据* @param jws* @return* @throws CertificateException*/public static AppStoreNotifyDto 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);// 验证 tokenAlgorithm algorithm = Algorithm.ECDSA256((ECPublicKey) publicKey, null);try {algorithm.verify(decodedJWT);} catch (SignatureVerificationException e) {log.error("解密苹果数据失败", e);throw  new AppException("解密苹果数据失败");}// 解析数据String decodeString = new String(java.util.Base64.getDecoder().decode(decodedJWT.getPayload()));return JSON.parseObject(decodeString, AppStoreNotifyDto.class);}/*** 解析事务数据* @param appStoreNotifyDto* @return*/public static AppStoreDecodedPayloadDto parseTransactionInfo(AppStoreNotifyDto appStoreNotifyDto) {DecodedJWT decode = JWT.decode(appStoreNotifyDto.getData().getSignedTransactionInfo());String decodeString = new String(Base64.getDecoder().decode(decode.getPayload()));return JSON.parseObject(decodeString, AppStoreDecodedPayloadDto.class);}/*** 获取公钥* @param x5c* @return* @throws CertificateException*/private static 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();}
}

这些都是固定写法,放上去就行了,没啥好说的,相关的java Bean也贴出来吧,放在下面

/*** zxc_user* time: 2023-11-17 15:34:47* @description:  解密核心数据** 参考地址: https://developer.apple.com/documentation/appstoreservernotifications/jwstransactiondecodedpayload?language=objc*/
@Data
public class AppStoreDecodedPayloadDto implements Serializable {private static final long serialVersionUID = 1L;///退款订单必存的字段/*** 应用的bundle标识符*/private String bundleId;/*** 与price参数相关联的三个字母的ISO 4217货币代码。此值仅在存在price时才存在*/private String currency;/*** 服务器环境,沙箱或生产环境。   sandbox or production*/private String environment;/*** 包含优惠代码或促销优惠标识符的标识符。*/private String offerIdentifier;/*** 表示促销优惠类型的值*/private String offerType;/*** UNIX时间,以毫秒为单位,表示原始事务标识符的购买日期。*/private String originalPurchaseDate;/*** 原始购买的交易标识符。*/private String originalTransactionId;/*** 一个整数值,表示您在App Store Connect中配置的应用内购买或订阅报价的价格乘以1000,并在购买时系统记录。有关更多信息,请参阅价格。currency参数表示此价格的货币。*/private String price;/*** 应用内购买的产品标识符。*/private String productId;/*** 用户购买的消耗品数量。*/private String quantity;/*** UNIX时间,以毫秒为单位,App Store在过期后向用户帐户收取购买、恢复产品、订阅或续订费用。*/private String purchaseDate;/*** UNIX时间,以毫秒为单位,应用商店将交易退款或从家庭共享中撤销交易*/private String revocationDate;/*** App Store退还交易或从家庭共享中撤销交易的原因。*/private String revocationReason;/*** 事务的唯一标识符。*/private String transactionId;/***  购买事务的原因,这表明它是客户购买还是系统启动的自动续订订阅的续订。*/private String transactionReason;/*** 应用内购买的类型。*/private String type;///跟订阅相关//*** 订阅到期或更新的UNIX时间,以毫秒为单位。   跟订阅相关*/private String expiresDate;/*** 一个布尔值,指示客户是否升级到另一个订阅。  跟订阅相关*/private boolean isUpgraded;/*** 订阅服务使用的付费模式,如免费试用、按需付费或预先付费 ,跟订阅相关*/private String offerDiscountType;/*** 订阅所属的订阅组的标识符。 跟订阅相关*/private String subscriptionGroupIdentifier;///其他相关//*** 您在购买时创建的UUID,它将交易与您自己服务上的客户关联起来。如果你的应用没有提供appAccountToken,这个字符串是空的。更多信息请参见appAccountToken(_:)。*/private String appAccountToken;/*** 一个字符串,描述该事务是由客户购买的,还是通过家庭共享提供给客户*/private String inAppOwnershipType;/*** UNIX时间,以毫秒为单位,应用商店签署JSON Web签名(JWS)数据的时间。*/private String signedDate;/*** 三个字母的代码,表示与购买的App Store店面相关的国家或地区。*/private String storefront;/*** 一个apple定义的值,唯一标识与购买相关的App Store店面。*/private String storefrontId;/*** 跨设备订阅购买事件的唯一标识符,包括订阅续订。*/private String webOrderLineItemId;
}
/*** zxc_user* time: 2023-11-17 15:22:08* @description:  苹果V2版本回调通知返回数据*** 参考官方地址:    https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload?language=objc*/
@Data
public class AppStoreNotifyDto implements Serializable {private static final long serialVersionUID = 1L;/*** 回调类型, 最主要的,等于REFUND是眼用户退款事件** 参考地址:https://developer.apple.com/documentation/appstoreservernotifications/notificationtype?language=objc*/private String notificationType;/*** 通知的唯一标识符。使用此值来标识重复的通知。*/private String notificationUUID;/*** 标识通知事件的其他信息。子类型字段仅用于特定的版本2通知。*/private String subtype;/*** 核心数据,退款信息之类的都在里面*/private AppStoreNotifyDataDto data;private String summary;/*** 通知版本号,V2*/private String version;/*** UNIX时间,以毫秒为单位*/private String signedDate;
}

操作步骤:

        就是把AppStoreNotifyPayLoadDto对象里面的signedPayload传到AppStoreReturnUtil工具类的verifyAndGet即可,便可以获得基础数据

        如果获取退款数据再调用一下AppStoreReturnUtil的parseTransactionInfo即可,

        记得如果只是处理退款的需要注意一下AppStoreNotifyDto对象的notificationType类型当等于REFUND才是退款,其他的业务请参考官方文档,notificationType | Apple Developer Documentation

到这里就行了,剩下的就是你要处理的业务逻辑,每个人的可能不太一样,这里就不赘述了

两者对比

        主动查询需要消耗你的性能,而且你不知道终止条件是啥,因为用户是随时可以向苹果发起退款申请的,虽然网上有人说下单后90天就不能,但是是不是也不确定....

        其次主动查询需要那些kid,k8文件等数据记录(这里可以理解为私钥),所以还是比较麻烦的

        被动接收相对就非常方便了,只需要配置url,然后提供控制器接收数据即可,这里是不需要kid,k8文件那些的(这里我理解是公钥在jar包里面提供的)而且可以节省你服务器性能

        所以我目前是选择了被动接收处理

设计模式使用

        这里我是用了command进行设计的,目前还没整理文档,后续整理了可以放出来大家讨论讨论

结语

        这里再次感谢开头放置的那些文章地址,说的挺详细了,因为我英文也不是很好,如果没有这些文章可能还挺麻烦

        整个流程其实并不难,就是以前没接过苹果的,所以刚开始有点懵逼,不过真正搞懂了其实也就那样

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/160932.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

试试MyBatis-Plus可视化代码生成器,太香了,你一定会感谢我

前言 在基于Mybatis的开发模式中&#xff0c;很多开发者还会选择Mybatis-Plus来辅助功能开发&#xff0c;以此提高开发的效率。虽然Mybatis也有代码生成的工具&#xff0c;但Mybatis-Plus由于在Mybatis基础上做了一些调整&#xff0c;因此&#xff0c;常规的生成工具生成的代码…

PC端使子组件的弹框关闭

子组件 <template><el-dialog title"新增部门" :visible"showDialog" close"close"> </el-dialog> </template> <script> export default {props: {showDialog: {type: Boolean,default: false,},},data() {retu…

Java面向对象(高级)-- 类中属性赋值的位置及过程

文章目录 一、赋值顺序&#xff08;1&#xff09;赋值的位置及顺序&#xff08;2&#xff09;举例&#xff08;3&#xff09;字节码文件&#xff08;4&#xff09;进一步探索&#xff08;5&#xff09;最终赋值顺序&#xff08;6&#xff09;实际开发如何选 二、(超纲)关于字节…

1992-2021年省市县经过矫正的夜间灯光数据(GNLD、VIIRS)

1992-2021年省市县经过矫正的夜间灯光数据&#xff08;GNLD、VIIRS&#xff09; 1、时间&#xff1a;1992-2021年3月&#xff0c;其中1992-2013年为年度数据&#xff0c;2013-2021年3月为月度数据 2、来源&#xff1a;DMSP、VIIRS 3、范围&#xff1a;分区域汇总&#xff1a…

SpringBoot : ch05 整合Mybatis

前言 随着Java Web应用程序的快速发展&#xff0c;开发人员需要越来越多地关注如何高效地构建可靠的应用程序。Spring Boot作为一种快速开发框架&#xff0c;旨在简化基于Spring的应用程序的初始搭建和开发过程。而MyBatis作为一种优秀的持久层框架&#xff0c;提供了对数据库…

【Linux】-进程间通信-共享内存(SystemV),详解接口函数以及原理(使用管道处理同步互斥机制)

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你 …

中低压MOSFET 2N7002T 60V 300mA 双N通道 采用SOT-523封装形式

2N7002KW小电流双N通道MOSFET&#xff0c;电压60V电流300mA&#xff0c;采用SOT-523封装形式。低Ros (on)的高密度单元设计&#xff0c;坚固可靠&#xff0c;具有高饱和电流能力&#xff0c;ESD防护门HBM2KV。可应用于直流/直流转换器&#xff0c;电池开关等产品应用上。

成为AI产品经理——AI产品经理工作全流程

一、业务背景 背景&#xff1a;日常排球训练&#xff0c;中考排球项目和排球体测项目耗费大量人力成本和时间成本。 目标&#xff1a;开发一套用于实时检测排球运动并进行排球垫球计数和姿势分析的软件。 二、产品工作流程 我们这里对于产品工作流程的关键部分进行讲解&…

「Docker」如何在苹果电脑上构建简单的Go云原生程序「MacOS」

介绍 使用Docker开发Golang云原生应用程序&#xff0c;使用Golang服务和Redis服务 注&#xff1a;写得很详细 为方便我的朋友可以看懂 环境部署 确保已经安装Go、docker等基础配置 官网下载链接直达&#xff1a;Docker官网下载 Go官网下载 操作步骤 第一步 创建一个…

汽车智能座舱/智能驾驶SOC -2

第二篇&#xff08;笔记&#xff09;。 未来智能汽车电子电气将会是集中式架构&#xff08;车载数据中心&#xff09;虚拟化技术&#xff08;提供车载数据中心灵活性和安全性&#xff09;这个几乎是毋庸置疑的了。国际大厂也否纷纷布局超算芯片和车载数据中心平台。但是演进需…

日期格式转化成星期几部署到linux显示英文

异常收集 原因&#xff1a;解决办法仰天大笑出门去&#xff0c;我辈岂是蓬蒿人 传入一个时间获取这个时间对应的是星期几&#xff0c;在开发环境&#xff08;window系统&#xff09;中显示为星期几&#xff0c;部署到服务器&#xff08;linux系统&#xff09;中会显示英文的时间…

Youtube新手运营——你需要的技巧与工具

对于有跨境意向的内容创作者或者品牌企业来说&#xff0c;YouTube是因其巨大的潜在受众群和商业价值成为最值得投入变现与营销计划的平台。 据统计&#xff0c;98% 的美国人每月访问 YouTube&#xff0c;近三分之二的人每天访问。但是&#xff0c;YouTube还远未达到过度饱和的…

Leetcode—53.最大子数组和【中等】

2023每日刷题&#xff08;三十四&#xff09; Leetcode—53.最大子数组和 前缀和算法思想 参考灵茶山艾府 实现代码 #define MAX(a, b) ((a > b) ? (a) : (b)) #define MIN(a, b) ((a < b) ? (a) : (b)) int maxSubArray(int* nums, int numsSize) {int ans INT_…

VMware 16 Pro 安装以及下载

1、下载地址&#xff1a; https://www.aliyundrive.com/s/nj3PSD4TN9G 2、安装文件 右击打开 下一步 密钥&#xff1a;ZF3R0-FHED2-M80TY-8QYGC-NPKYF 到此&#xff0c;安装完毕

postgreSQL如何快速查询大表数据量

文章目录 场景方案结果 场景 我有一个非常大的表&#xff0c;估计几百万或者几千万。 我开始使用了 select count(*) from my_table_javapub 方式&#xff0c;查询非常慢。 如何解决&#xff1f;&#xff1f;&#xff1f; 方案 如果你需要更快地获取表中的行数&#xff0c…

93.STL-系统内置仿函数

目录 算术仿函数 关系仿函数 逻辑仿函数 C 标准库中提供了一些内置的函数对象&#xff0c;也称为仿函数&#xff0c;它们通常位于 <functional> 头文件中。以下是一些常见的系统内置仿函数&#xff1a; 算术仿函数 功能描述&#xff1a; 实现四则运算其中negate是一元…

Java游戏之飞翔的小鸟

前言 飞翔的小鸟 小游戏 可以作为 java入门阶段的收尾作品 &#xff1b; 需要掌握 面向对象的使用以及了解 多线程&#xff0c;IO流&#xff0c;异常处理&#xff0c;一些java基础等相关知识。一 、游戏分析 1. 分析游戏逻辑 &#xff08;1&#xff09;先让窗口显示出来&#x…

腾讯待办导出的文件在哪找?支持打开ics文件的提醒待办工具

您使用过腾讯待办吗&#xff1f;如果您在平常使用的提醒待办工具为腾讯待办&#xff0c;想必近期您打开这款提醒待办工具时会看到提示您及时导出数据的提示。腾讯旗下的腾讯待办应用&#xff0c;应业务发展方向调整将于2023年12月20日全面停止运营并下架该应用。 面对突如其来…

Redis的主从复制及哨兵模式

一、Redis的主从复制 1.1 Redis主从复制定义 主从复制是redis实现高可用的基础&#xff0c;哨兵模式和集群都是在主从复制的基础之上实现高可用&#xff1b; 主从复制实现数据的多级备份&#xff0c;以及读写分离(主服务器负责写&#xff0c;从服务器只能读) 1.2 主从复制流…