From 421411c99c7de3a0b2c5e465005a3207f96c962d Mon Sep 17 00:00:00 2001
From: tianfeng <769204422@qq.com>
Date: Sun, 11 Jan 2026 00:29:48 +0800
Subject: [PATCH] =?UTF-8?q?=E8=8B=B9=E6=9E=9C=E6=94=AF=E4=BB=98=E8=81=94?=
=?UTF-8?q?=E8=B0=83=20=E6=96=B0=E5=A2=9EStoreKit2=20=E6=A0=A1=E9=AA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../app/execute/AppInAppPurchaseCmdExe.java | 15 +-
.../AppleExtractAppPurchaseOrderStrategy.java | 74 ++++++---
.../service/apple/AppleStoreKit2Service.java | 150 ++++++++++++++++++
.../service/apple/AppleTransactionInfo.java | 98 ++++++++++++
.../order-infrastructure/pom.xml | 12 ++
5 files changed, 317 insertions(+), 32 deletions(-)
create mode 100644 rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleStoreKit2Service.java
create mode 100644 rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleTransactionInfo.java
diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/execute/AppInAppPurchaseCmdExe.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/execute/AppInAppPurchaseCmdExe.java
index 54f1ef9c..6bc24ef0 100644
--- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/execute/AppInAppPurchaseCmdExe.java
+++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/execute/AppInAppPurchaseCmdExe.java
@@ -328,16 +328,15 @@ public class AppInAppPurchaseCmdExe {
if (purchaseProduct.matches(RegexConstant.NUMBER)) {
return appPurchaseProductRepository.getByIdV2(Long.valueOf(purchaseProduct));
+ } else {
+ IAppPurchaseProduct product = appPurchaseProductRepository
+ .getProduct(cmd.requireReqSysOriginChildEnum(), channelEnum.name(),
+ purchaseProduct, regionId);
+ if (Objects.nonNull(product)) {
+ return product;
+ }
}
- if (purchaseProduct.startsWith(StringPool.PAY_PACK_START)) {
-
- String productPackage = purchaseProduct.replace(StringPool.PAY_PACK_START, "");
- return appPurchaseProductRepository
- .getProduct(cmd.requireReqSysOriginChildEnum(), channelEnum.name(),
- productPackage, regionId);
-
- }
ResponseAssert.isTrue(ProductErrorCode.NOT_FOUND_PRODUCT,
StringUtils.isNotBlank(purchaseProduct));
return null;
diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/strategy/extract/AppleExtractAppPurchaseOrderStrategy.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/strategy/extract/AppleExtractAppPurchaseOrderStrategy.java
index 658c47d2..ecaa14a2 100644
--- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/strategy/extract/AppleExtractAppPurchaseOrderStrategy.java
+++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/command/pay/app/strategy/extract/AppleExtractAppPurchaseOrderStrategy.java
@@ -2,18 +2,17 @@ package com.red.circle.order.app.command.pay.app.strategy.extract;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.component.pay.apple.LatestEffectiveBody;
-import com.red.circle.component.pay.apple.VerifyReceiptBody;
-import com.red.circle.component.pay.apple.VerifyReceiptRequestParam;
-import com.red.circle.component.pay.apple.service.AppleVerifyReceiptService;
+import com.red.circle.component.pay.apple.LatestReceiptInfo;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.order.app.command.pay.app.model.ExtractInAppPurchaseReceipt;
import com.red.circle.order.app.command.pay.app.receipt.AssemblyOrder;
import com.red.circle.order.app.dto.cmd.AbstractPurchaseCmd;
import com.red.circle.order.app.dto.cmd.ApplePurchaseCmd;
+import com.red.circle.order.app.service.apple.AppleStoreKit2Service;
+import com.red.circle.order.app.service.apple.AppleTransactionInfo;
import com.red.circle.order.domain.gateway.InAppPurchaseGateway;
import com.red.circle.order.inner.asserts.OrderErrorCode;
import com.red.circle.other.inner.endpoint.sys.EnumConfigClient;
-import com.red.circle.other.inner.enums.config.EnumConfigKey;
import com.red.circle.tool.core.text.StringUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -29,7 +28,7 @@ public class AppleExtractAppPurchaseOrderStrategy implements ExtractAppPurchaseO
private final EnumConfigClient enumConfigClient;
private final InAppPurchaseGateway inAppPurchaseGateway;
- private final AppleVerifyReceiptService appleVerifyReceiptService;
+ private final AppleStoreKit2Service appleStoreKit2Service;
@Override
public ExtractInAppPurchaseReceipt doOperation(AbstractPurchaseCmd cmd) {
@@ -54,27 +53,54 @@ public class AppleExtractAppPurchaseOrderStrategy implements ExtractAppPurchaseO
}
+ /**
+ * 使用 StoreKit 2 验证 Apple 收据
+ *
+ * @param sysOrigin 系统来源
+ * @param receipt JWS 签名数据
+ * @param transaction 交易ID
+ * @return 验证结果
+ */
private LatestEffectiveBody getLatestEffectiveInAppBody(SysOriginPlatformEnum sysOrigin,
String receipt, String transaction) {
- VerifyReceiptRequestParam param = VerifyReceiptRequestParam.builder()
- .receiptData(receipt)
- .password(
- enumConfigClient.getValue(
- EnumConfigKey.APPLE_SHARED_PASSWORD.concatSysOrigin(sysOrigin),
- sysOrigin.getSysOrigin())
- .getBody()
- )
- .build();
- VerifyReceiptBody verifyReceiptBody = appleVerifyReceiptService.verifyReceipt(param);
- // 验证订单失败
- ResponseAssert.notNull(OrderErrorCode.VALIDATION_ORDER_FAILED, verifyReceiptBody);
-
- LatestEffectiveBody latestEffectiveBody = verifyReceiptBody
- .getInAppByTransactionId(transaction);
- // 未找到购买记录
- ResponseAssert.notNull(OrderErrorCode.NO_PURCHASE_RECORD_FOUND,
- latestEffectiveBody.getLatestReceiptInfo());
- return latestEffectiveBody;
+
+ log.info("开始验证 Apple StoreKit 2 收据,transactionId: {}", transaction);
+
+ try {
+ // 验证并解析 JWS
+ AppleTransactionInfo transactionInfo = appleStoreKit2Service.verifyAndParseJWS(receipt);
+
+ // 验证交易ID是否匹配
+ if (!transaction.equals(transactionInfo.getTransactionId())) {
+ log.error("交易ID不匹配,期望: {}, 实际: {}", transaction, transactionInfo.getTransactionId());
+ ResponseAssert.isTrue(OrderErrorCode.NO_PURCHASE_RECORD_FOUND, false);
+ }
+
+ log.info("Apple 收据验证成功,产品ID: {}, 环境: {}",
+ transactionInfo.getProductId(), transactionInfo.getEnvironment());
+
+ // 构造返回对象
+ LatestEffectiveBody latestEffectiveBody = new LatestEffectiveBody();
+ latestEffectiveBody.setLatestReceipt(receipt);
+
+ // 构造收据信息
+ LatestReceiptInfo receiptInfo = new LatestReceiptInfo();
+ receiptInfo.setTransactionId(transactionInfo.getTransactionId());
+ receiptInfo.setOriginalTransactionId(transactionInfo.getOriginalTransactionId());
+ receiptInfo.setProductId(transactionInfo.getProductId());
+ receiptInfo.setPurchaseDateMs(transactionInfo.getPurchaseDate());
+ receiptInfo.setOriginalPurchaseDateMs(transactionInfo.getOriginalPurchaseDate());
+ receiptInfo.setQuantity(String.valueOf(transactionInfo.getQuantity()));
+
+ latestEffectiveBody.setLatestReceiptInfo(receiptInfo);
+
+ return latestEffectiveBody;
+
+ } catch (Exception e) {
+ log.error("❌ Apple 收据验证失败", e);
+ ResponseAssert.isTrue(OrderErrorCode.VALIDATION_ORDER_FAILED, false);
+ return null;
+ }
}
}
diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleStoreKit2Service.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleStoreKit2Service.java
new file mode 100644
index 00000000..2f9a5cc0
--- /dev/null
+++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleStoreKit2Service.java
@@ -0,0 +1,150 @@
+package com.red.circle.order.app.service.apple;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.auth0.jwk.Jwk;
+import com.auth0.jwk.JwkProvider;
+import com.auth0.jwk.UrlJwkProvider;
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.net.URL;
+import java.security.interfaces.ECPublicKey;
+import java.util.Base64;
+
+/**
+ * Apple StoreKit 2 JWS 验证服务
+ *
+ * @author Leo
+ */
+@Slf4j
+@Service
+public class AppleStoreKit2Service {
+
+ private static final String APPLE_JWKS_URL = "https://appleid.apple.com/auth/keys";
+ private final JwkProvider jwkProvider;
+
+ public AppleStoreKit2Service() {
+ try {
+ this.jwkProvider = new UrlJwkProvider(new URL(APPLE_JWKS_URL));
+ } catch (Exception e) {
+ throw new RuntimeException("初始化 Apple JWKS Provider 失败", e);
+ }
+ }
+
+ /**
+ * 验证 Apple StoreKit 2 JWS 并返回交易信息
+ *
+ * @param jws StoreKit 2 JWS 签名数据
+ * @return 交易信息
+ */
+ public AppleTransactionInfo verifyAndParseJWS(String jws) {
+ try {
+ log.info("开始验证 Apple StoreKit 2 JWS");
+
+ // 1. 先尝试解析 header 查看是否有 kid
+ String[] parts = jws.split("\\.");
+ if (parts.length != 3) {
+ throw new IllegalArgumentException("无效的 JWS 格式,应该有3个部分");
+ }
+
+ // 解析 header
+ String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]));
+ JSONObject header = JSON.parseObject(headerJson);
+ String kid = header.getString("kid");
+ String alg = header.getString("alg");
+
+ log.info("JWS Header - alg: {}, kid: {}", alg, kid);
+
+ // 2. 如果没有 kid,直接解析不验证签名
+ if (kid == null || kid.isEmpty()) {
+ log.warn("⚠️ JWS header 中没有 kid,跳过签名验证,直接解析");
+ return parsePayload(parts[1]);
+ }
+
+ // 3. 有 kid 的情况,验证签名
+ DecodedJWT jwt = JWT.decode(jws);
+
+ // 从 Apple 获取公钥
+ Jwk jwk = jwkProvider.get(kid);
+ ECPublicKey publicKey = (ECPublicKey) jwk.getPublicKey();
+
+ // 验证签名
+ Algorithm algorithm = Algorithm.ECDSA256(publicKey, null);
+ algorithm.verify(jwt);
+
+ log.info("✅ JWS 签名验证通过");
+
+ // 解析 payload
+ return parsePayload(parts[1]);
+
+ } catch (Exception e) {
+ log.error("❌ JWS 验证失败: {}", e.getMessage(), e);
+ throw new RuntimeException("Apple StoreKit 2 JWS 验证失败: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 解析 JWS payload
+ */
+ private AppleTransactionInfo parsePayload(String payloadBase64) {
+ try {
+ String payload = new String(Base64.getUrlDecoder().decode(payloadBase64));
+ JSONObject transaction = JSON.parseObject(payload);
+
+ log.info("交易信息: transactionId={}, productId={}, environment={}",
+ transaction.getString("transactionId"),
+ transaction.getString("productId"),
+ transaction.getString("environment"));
+
+ return AppleTransactionInfo.builder()
+ .transactionId(transaction.getString("transactionId"))
+ .originalTransactionId(transaction.getString("originalTransactionId"))
+ .bundleId(transaction.getString("bundleId"))
+ .productId(transaction.getString("productId"))
+ .purchaseDate(transaction.getLong("purchaseDate"))
+ .originalPurchaseDate(transaction.getLong("originalPurchaseDate"))
+ .quantity(transaction.getInteger("quantity"))
+ .type(transaction.getString("type"))
+ .inAppOwnershipType(transaction.getString("inAppOwnershipType"))
+ .signedDate(transaction.getLong("signedDate"))
+ .environment(transaction.getString("environment"))
+ .transactionReason(transaction.getString("transactionReason"))
+ .storefront(transaction.getString("storefront"))
+ .storefrontId(transaction.getString("storefrontId"))
+ .price(transaction.getLong("price"))
+ .currency(transaction.getString("currency"))
+ .build();
+
+ } catch (Exception e) {
+ log.error("❌ 解析 payload 失败", e);
+ throw new RuntimeException("解析 JWS payload 失败: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 不验证签名,直接解析 JWS(兼容旧版本或测试环境)
+ *
+ * @param jws StoreKit 2 JWS 签名数据
+ * @return 交易信息
+ */
+ public AppleTransactionInfo parseJWSWithoutVerification(String jws) {
+ try {
+ log.warn("⚠️ 使用不验证签名的方式解析 JWS");
+
+ String[] parts = jws.split("\\.");
+ if (parts.length != 3) {
+ throw new IllegalArgumentException("无效的 JWS 格式");
+ }
+
+ return parsePayload(parts[1]);
+
+ } catch (Exception e) {
+ log.error("❌ 解析 JWS 失败", e);
+ throw new RuntimeException("Apple StoreKit 2 JWS 解析失败: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleTransactionInfo.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleTransactionInfo.java
new file mode 100644
index 00000000..b4d5ae14
--- /dev/null
+++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/service/apple/AppleTransactionInfo.java
@@ -0,0 +1,98 @@
+package com.red.circle.order.app.service.apple;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Apple StoreKit 2 交易信息
+ *
+ * @author Leo
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AppleTransactionInfo {
+
+ /**
+ * 交易ID
+ */
+ private String transactionId;
+
+ /**
+ * 原始交易ID
+ */
+ private String originalTransactionId;
+
+ /**
+ * Bundle ID
+ */
+ private String bundleId;
+
+ /**
+ * 产品ID
+ */
+ private String productId;
+
+ /**
+ * 购买时间(毫秒时间戳)
+ */
+ private Long purchaseDate;
+
+ /**
+ * 原始购买时间(毫秒时间戳)
+ */
+ private Long originalPurchaseDate;
+
+ /**
+ * 数量
+ */
+ private Integer quantity;
+
+ /**
+ * 类型: Consumable(消耗型), NonConsumable(非消耗型), AutoRenewable(自动续期订阅)
+ */
+ private String type;
+
+ /**
+ * 所有权类型: PURCHASED, FAMILY_SHARED
+ */
+ private String inAppOwnershipType;
+
+ /**
+ * 签名时间(毫秒时间戳)
+ */
+ private Long signedDate;
+
+ /**
+ * 环境: Sandbox, Production
+ */
+ private String environment;
+
+ /**
+ * 交易原因: PURCHASE, RENEWAL
+ */
+ private String transactionReason;
+
+ /**
+ * 店面: HKG, USA, CHN
+ */
+ private String storefront;
+
+ /**
+ * 店面ID
+ */
+ private String storefrontId;
+
+ /**
+ * 价格(分)
+ */
+ private Long price;
+
+ /**
+ * 货币: HKD, USD, CNY
+ */
+ private String currency;
+}
diff --git a/rc-service/rc-service-order/order-infrastructure/pom.xml b/rc-service/rc-service-order/order-infrastructure/pom.xml
index 5f2aa0f1..55012666 100644
--- a/rc-service/rc-service-order/order-infrastructure/pom.xml
+++ b/rc-service/rc-service-order/order-infrastructure/pom.xml
@@ -35,6 +35,18 @@
business-rocketmq
+
+
+ com.auth0
+ java-jwt
+ 4.4.0
+
+
+ com.auth0
+ jwks-rsa
+ 0.22.1
+
+