refactor(service): 重构Apple购买服务并新增一次性购买处理
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,4 +38,5 @@ public interface UserService extends IService<KeyboardUser> {
|
|||||||
*/
|
*/
|
||||||
Boolean cancelAccount(long userId);
|
Boolean cancelAccount(long userId);
|
||||||
|
|
||||||
|
Long selectUserByUUid(String uuid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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有效期
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user