东莞php网站建设价格,wordpress写的文章打不开,做兼职拍照片传网站,百度网盘资源免费搜索引擎入口前言 一般而言#xff0c;我们其实很少对接退款接口#xff0c;因为退款基本都是商家自己决定后进行操作的#xff0c;但是苹果比较特殊#xff0c;用户可以直接向苹果发起退款请求#xff0c;苹果觉得合理会退给用户#xff0c;但是目前公司业务还是需要对接这个接口我们其实很少对接退款接口因为退款基本都是商家自己决定后进行操作的但是苹果比较特殊用户可以直接向苹果发起退款请求苹果觉得合理会退给用户但是目前公司业务还是需要对接这个接口可能是以后为了对账之类使用的吧 本来对接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 --
dependencygroupIdcom.auth0/groupIdartifactIdjava-jwt/artifactIdversion3.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 {MapString, 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);RequestEntityMapString, String requestEntity new RequestEntity(header, HttpMethod.GET, URI.create(https://api.storekit-sandbox.itunes.apple.com/inApps/v2/refund/lookup/2000000308586738));ResponseEntityRefundHistResponseVO 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?languageobjc*/
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?languageobjc*/
Data
public class AppStoreNotifyDto implements Serializable {private static final long serialVersionUID 1L;/*** 回调类型, 最主要的,等于REFUND是眼用户退款事件** 参考地址:https://developer.apple.com/documentation/appstoreservernotifications/notificationtype?languageobjc*/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天就不能但是是不是也不确定.... 其次主动查询需要那些kidk8文件等数据记录(这里可以理解为私钥)所以还是比较麻烦的 被动接收相对就非常方便了只需要配置url然后提供控制器接收数据即可这里是不需要kidk8文件那些的(这里我理解是公钥在jar包里面提供的而且可以节省你服务器性能 所以我目前是选择了被动接收处理
设计模式使用 这里我是用了command进行设计的目前还没整理文档后续整理了可以放出来大家讨论讨论
结语 这里再次感谢开头放置的那些文章地址说的挺详细了因为我英文也不是很好如果没有这些文章可能还挺麻烦 整个流程其实并不难就是以前没接过苹果的所以刚开始有点懵逼不过真正搞懂了其实也就那样