refactor(service): 重构Apple购买服务并新增一次性购买处理

This commit is contained in:
2026-04-09 11:29:23 +08:00
parent 3665596c1f
commit cdfeace2f1
5 changed files with 148 additions and 16 deletions

View File

@@ -43,4 +43,11 @@ public interface ApplePurchaseService {
* @param notification 解码后的通知载荷 * @param notification 解码后的通知载荷
*/ */
void handleConsumptionRequest(ResponseBodyV2DecodedPayload notification); void handleConsumptionRequest(ResponseBodyV2DecodedPayload notification);
/**
* 处理一次性购买通知ONE_TIME_CHARGE
*
* @param notification 解码后的通知载荷
*/
void handleOneTimeChargeNotification(ResponseBodyV2DecodedPayload notification);
} }

View File

@@ -38,4 +38,5 @@ public interface UserService extends IService<KeyboardUser> {
*/ */
Boolean cancelAccount(long userId); Boolean cancelAccount(long userId);
Long selectUserByUUid(String uuid);
} }

View File

@@ -1,5 +1,6 @@
package com.yolo.keyborad.service.impl; 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.JWSRenewalInfoDecodedPayload;
import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload; import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload;
import com.apple.itunes.storekit.model.NotificationTypeV2; import com.apple.itunes.storekit.model.NotificationTypeV2;
@@ -34,6 +35,7 @@ import java.time.format.DateTimeParseException;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
/** /**
* 苹果购买后置处理:订阅续期 / 内购充值 + 记录落库 * 苹果购买后置处理:订阅续期 / 内购充值 + 记录落库
@@ -152,23 +154,10 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
JWSTransactionDecodedPayload transaction = JWSTransactionDecodedPayload transaction =
signedDataVerifier.verifyAndDecodeTransaction(signedTransactionInfo); signedDataVerifier.verifyAndDecodeTransaction(signedTransactionInfo);
String originalTransactionId = transaction.getOriginalTransactionId();
String productId = transaction.getProductId(); String productId = transaction.getProductId();
// 根据原始交易ID查询用户购买记录 UUID appAccountToken = transaction.getAppAccountToken();
List<KeyboardUserPurchaseRecords> records = purchaseRecordsService.lambdaQuery() Long userId = userService.selectUserByUUid(appAccountToken.toString());
.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();
// 查询商品信息 // 查询商品信息
KeyboardProductItems product = productItemsService.getProductEntityByProductId(productId); KeyboardProductItems product = productItemsService.getProductEntityByProductId(productId);
@@ -354,6 +343,127 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService {
// 提供用户消费状态、交付状态等信息,帮助 Apple 评估退款请求 // 提供用户消费状态、交付状态等信息,帮助 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<KeyboardUserPurchaseRecords> 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有效期 * 创建新的购买记录并延长VIP有效期

View File

@@ -155,6 +155,7 @@ public class AppleReceiptServiceImpl implements AppleReceiptService {
// 续订偏好变更通知 // 续订偏好变更通知
case DID_CHANGE_RENEWAL_PREF: case DID_CHANGE_RENEWAL_PREF:
case DID_CHANGE_RENEWAL_STATUS: case DID_CHANGE_RENEWAL_STATUS:
case PRICE_INCREASE: case PRICE_INCREASE:
applePurchaseService.handleRenewalPreferenceChange(notification); applePurchaseService.handleRenewalPreferenceChange(notification);
@@ -165,9 +166,14 @@ public class AppleReceiptServiceImpl implements AppleReceiptService {
applePurchaseService.handleConsumptionRequest(notification); applePurchaseService.handleConsumptionRequest(notification);
break; break;
// 一次性购买通知
case ONE_TIME_CHARGE:
applePurchaseService.handleOneTimeChargeNotification(notification);
break;
// 其他通知类型(记录但不处理) // 其他通知类型(记录但不处理)
case EXTERNAL_PURCHASE_TOKEN: case EXTERNAL_PURCHASE_TOKEN:
case ONE_TIME_CHARGE:
case REVOKE: case REVOKE:
case TEST: case TEST:
log.info("Received notification type {} - no action required", type); log.info("Received notification type {} - no action required", type);

View File

@@ -199,6 +199,14 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
return true; return true;
} }
@Override
public Long selectUserByUUid(String uuid) {
KeyboardUser keyboardUserDB = keyboardUserMapper.selectOne(
new LambdaQueryWrapper<KeyboardUser>()
.eq(KeyboardUser::getUuid, uuid));
return keyboardUserDB.getId();
}
private KeyboardUser buildNewUserWithSubjectId(String sub) { private KeyboardUser buildNewUserWithSubjectId(String sub) {
KeyboardUser keyboardUser = new KeyboardUser(); KeyboardUser keyboardUser = new KeyboardUser();
keyboardUser.setSubjectId(sub); keyboardUser.setSubjectId(sub);