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

news2025/1/19 3:39:54

前言

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

        本来对接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");
        // 密钥ID
        headers.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);

        // 验证 token
        Algorithm 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.coloradmin.cn/o/1239177.html

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

相关文章

2022年06月 Scratch(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 角色初始位置如图所示,下面哪个选项能让角色移到舞台的左下角? A: B: C: D: </

opencv-2D直方图

cv2.calcHist() 是 OpenCV 中用于计算直方图的函数。它可以计算一维或多维直方图&#xff0c;用于分析图像中像素值的分布。 基本的语法如下&#xff1a; hist cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])参数说明&#xff1a; images:…

10月起个税系统升级,3个月个税零申报将收到提示

近日&#xff0c;自然人电子税务局扣缴端升级了&#xff0c;升级后对于工资薪金收入连续三个月为零的纳税人&#xff0c;系统会自动出现以下提示。这个提示主要为了避免企业长期对已经离职的员工进行零申报&#xff0c;导致数据不准确和资源浪费。HR在申报个税时&#xff0c;一…

自动化测试学习指南

软件自动化测试的学习步骤 大概步骤如下&#xff1a; 1. 做好手工测试&#xff08;了解各种测试的知识&#xff09;-> 2. 学习编程语言-> 3. 学习Web基础&#xff08;HTML,HTTP,CSS,DOM,Javascript&#xff09;或者 学习Winform -> 4. 学习自动化测试工具 ->5.…

矩阵知识补充

正交矩阵 定义&#xff1a; 正交矩阵是一种满足 A T A E A^{T}AE ATAE的方阵 正交矩阵具有以下几个重要性质&#xff1a; A的逆等于A的转置&#xff0c;即 A − 1 A T A^{-1}A^{T} A−1AT**A的行列式的绝对值等于1&#xff0c;即 ∣ d e t ( A ) ∣ 1 |det(A)|1 ∣det(A)∣…

关于ego-planner里面的GridMap

浙大这套开源的代码写得很nice 很值得借鉴 &#xff0c; 对于 GridMap 类的实现。该类通过智能指针的封装简化了 GridMap 实例的创建和管理过程。一旦通过 GridMap::initMap(ros::NodeHandle &nh) 方法初始化&#xff0c;就可以方便地调用 GridMap 及其所有相关功能 它主要…

cocos2dx ​​Animate3D (一)

3D相关的动画都是继承Grid3DAction 本质上是用GirdBase进行创建动画的小块。 Shaky3D 晃动特效 // 持续时间(时间过后不会回到原来的样子) // 整个屏幕被分成几行几列 // 晃动的范围 // z轴是否晃动 static Shaky3D* create(float initWithDuration, const Size& …

Centos7安装Cesi(Supervisor集中管理工具)

Background CeSi 是 Supervisor 官方推荐的集中化管理 Supervisor 实例的 Web UI&#xff0c;该工具是用 Python 编写&#xff0c;基于 Flask Web 框架 。Superviosr 自带的 Web UI 不支持跨机器管理Supervisor 进程&#xff0c;功能比较简单&#xff0c;通过 CeSi 可以集中管理…

对线程的创建

一&#xff0c;概括 二&#xff0c;线程构建方式一&#xff08;继承Thread类&#xff09; 三&#xff0c;案例 父类&#xff1a; package Duoxiancheng;public abstract class Name {public static void main(String[] args) {//3&#xff0c;创建一个Thread线程类对象Thr…

python-opencv划痕检测-续

python-opencv划痕检测-续 这次划痕检测&#xff0c;是上一次划痕检测的续集。 处理的图像如下&#xff1a; 这次划痕检测&#xff0c;我们经过如下几步: 第一步&#xff1a;读取灰度图像 第二步&#xff1a;进行均值滤波 第三步&#xff1a;进行图像差分 第四步&#xff1…

你了解Postman 变量吗?

变量是在Postman工具中使用的一种特殊功能&#xff0c;用于存储和管理动态数据。它们可以用于在请求的不同部分、环境或集合之间共享和重复使用值。 Postman变量有以下几种类型&#xff1a; 1、环境变量&#xff08;Environment Variables&#xff09;: 环境变量是在Postman…

“我,24岁,年薪20万”:选对了行业究竟多重要?

那些在职场上顺风顺水&#xff0c;按部就班拿到高薪的人都有什么特点&#xff1f; 今天的主人公Flee告诉我&#xff0c;是稳。 在她的故事里&#xff0c;我看到一个“别人家的姑娘”&#xff0c;是怎样在职场上稳步晋升&#xff0c;大学毕业仅2年&#xff0c;就拿到18.6K月薪&a…

数字化建筑工地源码,施工全过程实时监控、数据分析、智能管理和优化调控

智慧工地是指通过信息化技术、物联网、人工智能技术等手段&#xff0c;对建筑工地进行数字化、智能化、网络化升级&#xff0c;实现对施工全过程的实时监控、数据分析、智能管理和优化调控。智慧工地的建设可以提高工地的安全性、效率性和质量&#xff0c;降低施工成本&#xf…

苍穹外卖项目笔记(4)——菜品管理

菜品管理 主要功能模块&#xff1a;新建菜品、修改菜品、启用禁用菜品、菜品的分页查询、删除菜品 代码&#xff1a;GitHub - Echo0701/take-out 1 公共字段自动填充 公共字段指的是业务表中有一些相同的字段&#xff0c;比如创建人、创建时间、修改人、修改时间等&#xff…

易点易动设备管理系统:提升企业设备巡检效率的最佳选择

在现代企业运营中&#xff0c;设备管理扮演着至关重要的角色。设备巡检旨在确保设备的正常运行和及时维护&#xff0c;以确保生产线的顺畅运行和业务的高效执行。然而&#xff0c;传统的设备巡检方法常常效率低下、耗时费力。针对这一问题&#xff0c;易点易动设备管理系统应运…

哈希表之开散列的实现

回顾与引出 我们在上一节用闭散列的开放定址法实现了哈希表。不难看出这种方法有明显的缺点&#xff1a;一旦发生哈希冲突&#xff0c;所有的冲突连在一起&#xff0c;容易产生数据“堆积”&#xff0c;即&#xff1a;不同 关键码占据了可利用的空位置&#xff0c;使得寻找某关…

【Mysql】[Err] 1293 - Incorrect table definition;

基本情况 SQL文件描述 /* Navicat MySQL Data TransferSource Server : cm4生产-200 Source Server Version : 50725 Source Host : 192.168.1.200:3306 Source Database : db_wmsTarget Server Type : MYSQL Target Server Version : 50725 File…

4G5G智能执法记录仪在保险公司车辆保险远程定损中的应用

4G智能执法记录仪&#xff1a;汽车保险定损的**利器 随着科技的不断进步&#xff0c;越来越多的智能设备应用到日常生活中。而在车辆保险定损领域&#xff0c;4G智能执法记录仪的出现无疑是一大**。它不仅可以实现远程定损&#xff0c;还能实现可视化操作、打印保单以及数据融…

10年经验之谈 —— 如何做接口测试呢?接口测试有哪些工具?

回想入职测试已经10年时间了&#xff0c;初入职场的我对于接口测试茫然不知。后来因为业务需要&#xff0c;开始慢慢接触接口测试。从最开始使用工具进行接口测试到编写代码实现接口自动化&#xff0c;到最后的测试平台开发。回想这一路走来感触颇深&#xff0c;因此为了避免打…

几个西门子PLC常见通讯问题的解决方法

1台200SMART 如何控制2台步进电机&#xff1f; S7-200SMART CPU最多可输出3路高速脉冲&#xff08;除ST20外&#xff09;&#xff0c;这意味着可同时控制最多3个步进电机&#xff0c;通过运动向导可配置相应的运动控制子程序&#xff0c;然后通过调用子程序编程可实现对步进电…