diff --git a/src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java b/src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java index 5baa1a3..b95ec87 100644 --- a/src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java +++ b/src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java @@ -43,4 +43,11 @@ public interface ApplePurchaseService { * @param notification 解码后的通知载荷 */ void handleConsumptionRequest(ResponseBodyV2DecodedPayload notification); + + /** + * 处理一次性购买通知(ONE_TIME_CHARGE) + * + * @param notification 解码后的通知载荷 + */ + void handleOneTimeChargeNotification(ResponseBodyV2DecodedPayload notification); } diff --git a/src/main/java/com/yolo/keyborad/service/UserService.java b/src/main/java/com/yolo/keyborad/service/UserService.java index f530601..550b910 100644 --- a/src/main/java/com/yolo/keyborad/service/UserService.java +++ b/src/main/java/com/yolo/keyborad/service/UserService.java @@ -38,4 +38,5 @@ public interface UserService extends IService { */ Boolean cancelAccount(long userId); + Long selectUserByUUid(String uuid); } diff --git a/src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java index e2be8e4..3461703 100644 --- a/src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java +++ b/src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java @@ -1,5 +1,6 @@ package com.yolo.keyborad.service.impl; +import cn.dev33.satoken.stp.StpUtil; import com.apple.itunes.storekit.model.JWSRenewalInfoDecodedPayload; import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload; import com.apple.itunes.storekit.model.NotificationTypeV2; @@ -34,6 +35,7 @@ import java.time.format.DateTimeParseException; import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.UUID; /** * 苹果购买后置处理:订阅续期 / 内购充值 + 记录落库 @@ -152,23 +154,10 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService { JWSTransactionDecodedPayload transaction = signedDataVerifier.verifyAndDecodeTransaction(signedTransactionInfo); - String originalTransactionId = transaction.getOriginalTransactionId(); String productId = transaction.getProductId(); - // 根据原始交易ID查询用户购买记录 - List records = purchaseRecordsService.lambdaQuery() - .eq(KeyboardUserPurchaseRecords::getOriginalTransactionId, originalTransactionId) - .orderByDesc(KeyboardUserPurchaseRecords::getId) - .last("LIMIT 1") - .list(); - - if (records == null || records.isEmpty()) { - log.warn("No purchase record found for originalTransactionId={}", originalTransactionId); - return; - } - - KeyboardUserPurchaseRecords existingRecord = records.get(0); - Long userId = existingRecord.getUserId().longValue(); + UUID appAccountToken = transaction.getAppAccountToken(); + Long userId = userService.selectUserByUUid(appAccountToken.toString()); // 查询商品信息 KeyboardProductItems product = productItemsService.getProductEntityByProductId(productId); @@ -354,6 +343,127 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService { // 提供用户消费状态、交付状态等信息,帮助 Apple 评估退款请求 } + /** + * 处理一次性购买通知(ONE_TIME_CHARGE) + * 流程参考 processPurchase: + * 1. 验证并解码交易信息 + * 2. 幂等性检查,避免重复处理 + * 3. 通过 originalTransactionId 查找用户 + * 4. 查询商品信息 + * 5. 保存购买记录 + * 6. 根据商品类型执行对应逻辑(订阅延期或钱包充值) + * + * @param notification 解码后的通知载荷 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void handleOneTimeChargeNotification(ResponseBodyV2DecodedPayload notification) { + if (notification == null || notification.getData() == null) { + log.warn("ONE_TIME_CHARGE notification data is null"); + return; + } + + try { + // 1. 解码交易信息 + String signedTransactionInfo = notification.getData().getSignedTransactionInfo(); + if (signedTransactionInfo == null || signedTransactionInfo.isBlank()) { + log.warn("No signed transaction info in ONE_TIME_CHARGE notification"); + return; + } + + JWSTransactionDecodedPayload transaction = + signedDataVerifier.verifyAndDecodeTransaction(signedTransactionInfo); + + String transactionId = transaction.getTransactionId(); + String originalTransactionId = transaction.getOriginalTransactionId(); + String productId = transaction.getProductId(); + UUID appAccountToken = transaction.getAppAccountToken(); + Long userId = userService.selectUserByUUid(appAccountToken.toString()); + + log.info("Processing ONE_TIME_CHARGE: transactionId={}, productId={}", transactionId, productId); + + // 2. 幂等性检查:根据交易ID判断是否已处理 + boolean handled = purchaseRecordsService.lambdaQuery() + .eq(KeyboardUserPurchaseRecords::getTransactionId, transactionId) + .eq(KeyboardUserPurchaseRecords::getStatus, "PAID") + .exists(); + if (handled) { + log.info("ONE_TIME_CHARGE already handled, transactionId={}", transactionId); + return; + } + +// // 3. 通过 originalTransactionId 查找关联的用户购买记录以获取 userId +// List records = purchaseRecordsService.lambdaQuery() +// .eq(KeyboardUserPurchaseRecords::getOriginalTransactionId, originalTransactionId) +// .orderByDesc(KeyboardUserPurchaseRecords::getId) +// .last("LIMIT 1") +// .list(); +// +// if (records == null || records.isEmpty()) { +// log.warn("No purchase record found for ONE_TIME_CHARGE, originalTransactionId={}", originalTransactionId); +// return; +// } + +// Long userId = records.get(0).getUserId().longValue(); + + // 4. 查询商品信息 + KeyboardProductItems product = productItemsService.getProductEntityByProductId(productId); + if (product == null) { + log.error("Product not found for ONE_TIME_CHARGE, productId={}", productId); + return; + } + // 5. 构建并保存购买记录 + KeyboardUserPurchaseRecords purchaseRecord = new KeyboardUserPurchaseRecords(); + purchaseRecord.setUserId(userId.intValue()); + purchaseRecord.setProductId(productId); + purchaseRecord.setPurchaseQuantity(product.getDurationValue()); + purchaseRecord.setPrice(product.getPrice()); + purchaseRecord.setCurrency(product.getCurrency()); + purchaseRecord.setPurchaseType(product.getType()); + purchaseRecord.setStatus("PAID"); + purchaseRecord.setPaymentMethod("APPLE"); + purchaseRecord.setTransactionId(transactionId); + purchaseRecord.setOriginalTransactionId(originalTransactionId); + purchaseRecord.setProductIds(new String[]{productId}); + + if (transaction.getPurchaseDate() != null) { + purchaseRecord.setPurchaseTime(Date.from(Instant.ofEpochMilli(transaction.getPurchaseDate()))); + purchaseRecord.setPurchaseDate(Date.from(Instant.ofEpochMilli(transaction.getPurchaseDate()))); + } else { + purchaseRecord.setPurchaseTime(new Date()); + } + + if (transaction.getExpiresDate() != null) { + purchaseRecord.setExpiresDate(Date.from(Instant.ofEpochMilli(transaction.getExpiresDate()))); + } + + if (transaction.getEnvironment() != null) { + purchaseRecord.setEnvironment(transaction.getEnvironment().name()); + } + + purchaseRecordsService.save(purchaseRecord); + + // 6. 根据商品类型执行对应的业务逻辑 + if ("subscription".equalsIgnoreCase(product.getType())) { + Instant expiresInstant = transaction.getExpiresDate() != null + ? Instant.ofEpochMilli(transaction.getExpiresDate()) : null; + extendVip(userId, product, expiresInstant); + } else if ("in-app-purchase".equalsIgnoreCase(product.getType())) { + handleInAppPurchase(userId, product, purchaseRecord.getId()); + } else { + log.warn("未知商品类型, type={}, productId={}", product.getType(), productId); + } + + log.info("ONE_TIME_CHARGE processed successfully: userId={}, transactionId={}", userId, transactionId); + + } catch (VerificationException e) { + log.error("Failed to verify transaction in ONE_TIME_CHARGE notification", e); + } catch (Exception e) { + log.error("Error processing ONE_TIME_CHARGE notification", e); + throw new BusinessException(ErrorCode.OPERATION_ERROR, "处理一次性购买通知失败"); + } + } + /** * 处理成功的续订 * 创建新的购买记录并延长VIP有效期 diff --git a/src/main/java/com/yolo/keyborad/service/impl/AppleReceiptServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/AppleReceiptServiceImpl.java index d74bf48..bc93792 100644 --- a/src/main/java/com/yolo/keyborad/service/impl/AppleReceiptServiceImpl.java +++ b/src/main/java/com/yolo/keyborad/service/impl/AppleReceiptServiceImpl.java @@ -155,6 +155,7 @@ public class AppleReceiptServiceImpl implements AppleReceiptService { // 续订偏好变更通知 case DID_CHANGE_RENEWAL_PREF: + case DID_CHANGE_RENEWAL_STATUS: case PRICE_INCREASE: applePurchaseService.handleRenewalPreferenceChange(notification); @@ -165,9 +166,14 @@ public class AppleReceiptServiceImpl implements AppleReceiptService { applePurchaseService.handleConsumptionRequest(notification); break; + // 一次性购买通知 + case ONE_TIME_CHARGE: + applePurchaseService.handleOneTimeChargeNotification(notification); + break; + // 其他通知类型(记录但不处理) case EXTERNAL_PURCHASE_TOKEN: - case ONE_TIME_CHARGE: + case REVOKE: case TEST: log.info("Received notification type {} - no action required", type); diff --git a/src/main/java/com/yolo/keyborad/service/impl/UserServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/UserServiceImpl.java index 8700196..0d161cb 100644 --- a/src/main/java/com/yolo/keyborad/service/impl/UserServiceImpl.java +++ b/src/main/java/com/yolo/keyborad/service/impl/UserServiceImpl.java @@ -199,6 +199,14 @@ public class UserServiceImpl extends ServiceImpl() + .eq(KeyboardUser::getUuid, uuid)); + return keyboardUserDB.getId(); + } + private KeyboardUser buildNewUserWithSubjectId(String sub) { KeyboardUser keyboardUser = new KeyboardUser(); keyboardUser.setSubjectId(sub);