refactor(apple-purchase): 重构苹果购买服务,增强可读性和健壮性

This commit is contained in:
2025-12-15 15:15:10 +08:00
parent a70c1f4049
commit d9a778f5aa
3 changed files with 327 additions and 31 deletions

View File

@@ -53,29 +53,61 @@ public class AppleReceiptController {
return ResultUtils.success(Boolean.TRUE);
}
/**
* 接收 Apple 服务器通知
* 处理来自 Apple 的服务器到服务器通知,主要用于订阅续订等事件
*
* @param body 请求体,包含 signedPayload 字段
* @param request HTTP 请求对象
* @return 处理结果
* @throws BusinessException 当 signedPayload 为空或解析失败时抛出
*/
@PostMapping("/notification")
public BaseResponse<Boolean> receiveNotification(@RequestBody Map<String, String> body, HttpServletRequest request) {
// 从请求体中获取 Apple 签名的载荷
String signedPayload = body.get("signedPayload");
log.warn(body.toString());
// 校验 signedPayload 是否为空
if (signedPayload == null || signedPayload.isBlank()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "signedPayload 不能为空");
}
// 解码签名载荷,获取通知详情
AppleServerNotification notification = decodeSignedPayload(signedPayload);
log.info("Apple server notification decoded: {}, query: {}", notification, request.getQueryString());
// 判断是否为续订相关通知,如果是则进行处理
if (notification != null && notification.getNotificationType() != null
&& notification.getNotificationType().toUpperCase().contains("RENEW")) {
applePurchaseService.processRenewNotification(notification);
}
return ResultUtils.success(Boolean.TRUE);
}
/**
* 解码 Apple 签名的 JWT 载荷
* 从 signedPayload 中解析出服务器通知的详细信息包括通知类型、环境、产品ID、交易信息等
*
* @param signedPayload Apple 服务器发送的签名载荷JWT 格式)
* @return AppleServerNotification 对象,包含解析后的通知详情
* @throws BusinessException 当参数无效或解析失败时抛出
*/
private AppleServerNotification decodeSignedPayload(String signedPayload) {
try {
// 解析外层 JWT 载荷
JsonNode root = JwtParser.parsePayload(signedPayload);
AppleServerNotification notification = new AppleServerNotification();
// 获取通知类型(支持驼峰和下划线两种命名格式)
notification.setNotificationType(text(root, "notificationType", "notification_type"));
// 解析 data 节点中的基本信息
JsonNode data = root.get("data");
if (data != null && !data.isNull()) {
notification.setEnvironment(text(data, "environment"));
@@ -83,14 +115,23 @@ public class AppleReceiptController {
notification.setOriginalTransactionId(text(data, "originalTransactionId", "original_transaction_id"));
notification.setSignedTransactionInfo(text(data, "signedTransactionInfo", "signed_transaction_info"));
}
// 如果存在签名的交易信息,进一步解析嵌套的 JWT
if (notification.getSignedTransactionInfo() != null) {
JsonNode txNode = JwtParser.parsePayload(notification.getSignedTransactionInfo());
// 从交易信息中提取详细字段
notification.setTransactionId(text(txNode, "transactionId", "transaction_id"));
// 优先使用外层的值,如果为空则使用交易信息中的值
notification.setProductId(firstNonBlank(notification.getProductId(), text(txNode, "productId", "product_id")));
notification.setOriginalTransactionId(firstNonBlank(notification.getOriginalTransactionId(), text(txNode, "originalTransactionId", "original_transaction_id")));
// 将时间戳转换为 ISO 8601 格式
notification.setPurchaseDate(epochToIso(txNode, "purchaseDate", "purchase_date"));
notification.setExpiresDate(epochToIso(txNode, "expiresDate", "expires_date"));
}
return notification;
} catch (IllegalArgumentException e) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, e.getMessage());
@@ -100,6 +141,14 @@ public class AppleReceiptController {
}
}
/**
* 从 JSON 节点中提取文本值
* 支持多个候选键名,按顺序尝试获取第一个非空值
*
* @param node JSON 节点
* @param keys 候选的键名列表(支持驼峰和下划线命名)
* @return 找到的第一个非空文本值,如果都不存在则返回 null
*/
private String text(JsonNode node, String... keys) {
for (String k : keys) {
if (node != null && node.has(k) && !node.get(k).isNull()) {
@@ -109,6 +158,14 @@ public class AppleReceiptController {
return null;
}
/**
* 返回第一个非空白的字符串
* 用于在多个可选值中选择优先级更高的非空值
*
* @param a 第一优先级的字符串
* @param b 第二优先级的字符串
* @return 第一个非空白的字符串,如果都为空则返回第一个参数
*/
private String firstNonBlank(String a, String b) {
if (a != null && !a.isBlank()) {
return a;
@@ -116,15 +173,25 @@ public class AppleReceiptController {
return (b != null && !b.isBlank()) ? b : a;
}
/**
* 将 Unix 时间戳(毫秒)转换为 ISO 8601 格式字符串
* 从 JSON 节点中提取时间戳并转换为标准 ISO 格式
*
* @param node JSON 节点
* @param keys 候选的时间戳字段键名
* @return ISO 8601 格式的时间字符串,如果解析失败则返回原始值
*/
private String epochToIso(JsonNode node, String... keys) {
String val = text(node, keys);
if (val == null) {
return null;
}
try {
// 解析时间戳并转换为 ISO 格式
long epochMillis = Long.parseLong(val);
return java.time.Instant.ofEpochMilli(epochMillis).toString();
} catch (NumberFormatException e) {
// 如果不是有效的数字,直接返回原值
return val;
}
}