refactor(apple-purchase): 重构苹果购买服务,增强可读性和健壮性
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user