对接苹果支付退款退单接口
前言
一般而言,我们其实很少对接退款接口,因为退款基本都是商家自己决定后进行操作的,但是苹果比较特殊,用户可以直接向苹果发起退款请求,苹果觉得合理会退给用户,但是目前公司业务还是需要对接这个接口,可能是以后为了对账之类使用的吧
本来对接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进行设计的,目前还没整理文档,后续整理了可以放出来大家讨论讨论
结语
这里再次感谢开头放置的那些文章地址,说的挺详细了,因为我英文也不是很好,如果没有这些文章可能还挺麻烦
整个流程其实并不难,就是以前没接过苹果的,所以刚开始有点懵逼,不过真正搞懂了其实也就那样
相关文章:
对接苹果支付退款退单接口
前言 一般而言,我们其实很少对接退款接口,因为退款基本都是商家自己决定后进行操作的,但是苹果比较特殊,用户可以直接向苹果发起退款请求,苹果觉得合理会退给用户,但是目前公司业务还是需要对接这个接口&am…...
合肥中科深谷嵌入式项目实战——基于ARM语音识别的智能家居系统(三)
基于ARM语音识别的智能家居系统 我们上一篇,我们实现在Linux系统下编译程序,我们首先通过两个小练习来熟悉一下如何去编译。今天,我们来介绍一下LCD屏幕基本使用。 一、LCD屏幕基本使用 如何使用LCD屏幕? 1、打开开发板LCD设…...
Web前端—移动Web第四天(vw适配方案、vw和vh的基本使用、综合案例-酷我音乐)
版本说明 当前版本号[20231122]。 版本修改说明20231122初版 目录 文章目录 版本说明目录移动 Web 第四天01-vw适配方案vw和vh基本使用vw布局vh布局混用问题 02-综合案例-酷我音乐准备工作头部布局头部内容搜索区域banner 区域标题公共样式排行榜内容推荐歌单布局推荐歌单内…...
报错注入 [极客大挑战 2019]HardSQL1
打开题目 输入1或者1",页面均回显NO,Wrong username password!!! 那我们输入1 试试万能密码 1 or 11 # 输入1 and 12 # 输入1 union select 1,2,3 # 输入1 ununionion seselectlect 1,2,3 # 输入1 # 输入1# 页面依旧回…...
【MATLAB源码-第83期】基于matlab的MIMO中V-BALST结构ZF和MMSE检测算法性能误码率对比。
操作环境: MATLAB 2022a 1、算法描述 在多输入多输出(MIMO)通信系统中,V-BLAST(垂直波束形成层间空间时间编码技术)是一种流行的技术,用于提高无线通信的数据传输速率和容量。它通过在不同的…...
Android13 新增 Stable AIDL接口
问题描述: 我需要在netd aidl 中添加新的接口: 设置网卡MAC地址: void setHardwareAddress(in utf8InCpp String iface, in utf8InCpp String hwAddr); 背景: Android 10 添加了对稳定的 Android 接口定义语言 (AIDL) 的支持&…...
Postman API Enterprise 10.18.1 Crack
适合您企业的 Postman API 平台 掌控您的 API 环境。构建更好的 API。加快产品开发。 无论您处于 API 之旅的哪个阶段,Postman 都会为您提供帮助 想让您团队的 API 更容易被发现吗?希望减少开发和质量检查之间的滞后时间?想要更快地让新开发…...
电脑内存升级
ddr代兼容 自从DDR内存时代开启之后,只要满足内存的插槽规格相同(DDR3或DDR4或DDR5即为内存规格)这一条件,不同品牌、不同频率以及不同容量的茶品都可以一起使用,除了品牌和容量的影响之外,不同频率的搭配可能会造成性能方面的影…...
ExcelBDD PHP Guideline
在PHP里面支持利用Excel的BDD,也支持利用Excel进行参数化测试 ExcelBDD Use Excel file as BDD feature file, get example data from Excel files, support automation tests. Features The main features provided by this library are: Read test data acco…...
C++静态链接库的生成以及使用
目录 一.前言二.生成静态链接库三.使用静态链接库四.其他 一.前言 这篇文章简单讨论一下VS如何生成和使用C静态链接库,示例使用VS2022环境。 二.生成静态链接库 先创建C项目-静态库 然后将默认生成的.h和.cpp文件清理干净,当然你也可以选择保留。 然…...
【2024系统架构设计】 系统架构设计师第二版-未来信息综合技术
目录 一 信息物理系统 二 人工智能 三 机器人技术 四 边缘计算 五 数字孪生体...
JavaFX修改软件图标
JavaFX默认使用jdk的程序图片显示,可以通过以下代码进行修改设置 stage.getIcons().add(new Image("static/icon.png")); static/icon.png改为自己图片路径 这里可以使用相对路径和绝对路径,看自己需求设置 例: import javafx.a…...
Linux ps -ef|grep去除 grep --color=auto信息
linux 监控 进程判断是否启动可通过该指令实现 ps -ef|grep java指令结果为 # -v 参数有过滤作用 ps -ef|grep java |grep -v grep...
jQuery的学习(一篇文章齐全)
目录 Day29 jQuery 1、jQuery介绍 2、jQuery的选择器 2.1、直接查找 2.2、导航查找 3、jQuery的绑定事件 案例1:绑定取消事件 案例2:模拟事件触发 4、jQuery的操作标签 tab切换案例jQuery版本: 案例1: 案例2ÿ…...
注塑行业各类业务流程图(系统化)
...
Android Studio 安装及使用
🍓 简介:java系列技术分享(👉持续更新中…🔥) 🍓 初衷:一起学习、一起进步、坚持不懈 🍓 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正🙏 🍓 希望这篇文章对你有所帮助,欢…...
计算机网络的OSI七层模型
目录 1、OSI七层模型是什么 1.1 物理层(Physical Layer) 1.2 数据链路层(Data Link Layer) 1.3 网络层(Network Layer) 1.4 传输层(Transport Layer) 1.5 会话层(S…...
如何一次性解压多个文件
第一步:多选压缩包 第二步:右键解压即可 一句话,单个怎么解压,多个就怎么解压,只不过先选中 参考:如何一次性解压多个文件...
类和对象学习笔记
类和对象 类的定义this指针类的6个默认成员函数构造函数析构函数拷贝构造函数赋值运算符重载赋值运算符重载运算符重载const成员 取地址操作符重载const取地址操作符重载 初始化列表explicit关键字static成员匿名对象友元内部类拷贝对象时编译器的优化 类的定义 c类的定义形式…...
Linux程序之可变参数选项那些事!
一、linux应用程序如何接收参数? 1. argc、argv Linux应用程序执行时,我们往往通过命令行带入参数给程序,比如 ls /dev/ -l 其中参数 /dev/ 、-l都是作为参数传递给命令 ls 应用程序又是如何接收这些参数的? 通常应用程序都…...
别再踩坑了!Windows下用Code::Blocks搭建LVGL模拟器(V9版)的完整避坑指南
Windows下用Code::Blocks搭建LVGL V9模拟器的完整避坑指南 最近在Windows平台上用Code::Blocks搭建LVGL V9模拟器时,发现网上大部分教程都是针对V8版本的,导致在文件系统访问环节频频踩坑。本文将分享我从环境准备到成功运行的全过程,特别是那…...
终极指南:一键合并B站缓存视频,完整保留弹幕体验
终极指南:一键合并B站缓存视频,完整保留弹幕体验 【免费下载链接】BilibiliCacheVideoMerge 🔥🔥Android上将bilibili缓存视频合并导出为mp4,支持安卓5.0 ~ 13,视频挂载弹幕播放(Android consolidates and …...
网盘直链下载终极指南:告别限速,拥抱全平台高速下载新时代
网盘直链下载终极指南:告别限速,拥抱全平台高速下载新时代 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 ,支持 百度网盘 / 阿里云盘 / 中国…...
突发:AISMM认证通道将于2026年Q2关闭旧版评估协议!现在不掌握V2.1动态基线,Q3招标直接出局
更多请点击: https://intelliparadigm.com 第一章:2026奇点智能技术大会:AISMM评估工具 AISMM(Artificial Intelligence System Maturity Model)评估工具是2026奇点智能技术大会正式发布的开源框架,旨在系…...
为自主AI智能体构建去中心化金融基础设施:ARS系统架构与实现
1. 项目概述:为自主智能体而生的去中心化储备系统如果你正在构建一个自主运行的AI智能体,或者对“智能体互联网”这个概念感到兴奋,那么你很可能已经遇到了一个核心难题:这些智能体之间如何高效、透明且无需人工干预地协调资本&am…...
通达信缠论可视化插件终极指南:3步实现专业级技术分析
通达信缠论可视化插件终极指南:3步实现专业级技术分析 【免费下载链接】Indicator 通达信缠论可视化分析插件 项目地址: https://gitcode.com/gh_mirrors/ind/Indicator 你是否曾经为缠论的复杂结构而头疼?面对K线图中的顶底分型、笔、线段和中枢…...
深入Linux内核:VFIO如何绕过KVM实现近乎裸机的I/O性能?一次讲透DMA与中断重映射
深入Linux内核:VFIO如何绕过KVM实现近乎裸机的I/O性能?一次讲透DMA与中断重映射 在虚拟化技术日新月异的今天,追求接近物理机性能的I/O虚拟化方案一直是开发者关注的焦点。传统虚拟化环境中,虚拟机对设备的访问需要经过层层抽象和…...
别再傻傻滚鼠标了!用CodeGlance Pro插件,5分钟搞定VS Code/IDEA代码全局导航
告别无效滚动:用CodeGlance Pro重塑代码导航体验 作为一名长期与复杂代码库打交道的开发者,你是否经历过这样的场景:在重构一个3000行的React组件时,反复滚动屏幕寻找某个关键函数;或者在调试时,需要不断在…...
明日方舟资源宝库:2000+高清素材如何改变你的创作游戏规则?
明日方舟资源宝库:2000高清素材如何改变你的创作游戏规则? 【免费下载链接】ArknightsGameResource 明日方舟客户端素材 项目地址: https://gitcode.com/gh_mirrors/ar/ArknightsGameResource 你是否曾为寻找高质量的游戏素材而苦恼?是…...
VisionMaster卡尺工具实战:5分钟搞定PCB焊盘间距测量(保姆级参数详解)
VisionMaster卡尺工具实战:PCB焊盘间距测量的工业级解决方案 在电子制造领域,PCB焊盘间距的精确测量直接关系到产品质量与可靠性。传统人工检测方式不仅效率低下,且难以满足微米级精度要求。VisionMaster的卡尺工具通过智能边缘检测算法&…...
