feat(apple): 新增服务器通知续订与JWT解析能力

- 支持解析Apple签名JWT并提取交易信息
- 新增processRenewNotification处理续订通知
- 添加测试用JWT生成、解析及发送重试记录示例
- 移除废弃ApplePayUtil,统一走新验证逻辑
This commit is contained in:
2025-12-15 14:56:38 +08:00
parent c1dd4faf0e
commit a70c1f4049
12 changed files with 528 additions and 91 deletions

View File

@@ -6,9 +6,11 @@ import com.yolo.keyborad.common.ErrorCode;
import com.yolo.keyborad.common.ResultUtils;
import com.yolo.keyborad.exception.BusinessException;
import com.yolo.keyborad.model.dto.AppleReceiptValidationResult;
import com.yolo.keyborad.model.dto.AppleServerNotification;
import com.yolo.keyborad.service.ApplePurchaseService;
import com.yolo.keyborad.service.AppleReceiptService;
import jakarta.servlet.http.HttpServletRequest;
import com.yolo.keyborad.utils.JwtParser;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -16,6 +18,8 @@ import org.springframework.web.bind.annotation.RestController;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@RestController
@RequestMapping("/apple")
@@ -50,10 +54,79 @@ public class AppleReceiptController {
}
@PostMapping("/notification")
public BaseResponse<Boolean> receiveNotification(@RequestBody Map<String, Object> body, HttpServletRequest request) {
log.info(request.getQueryString());
log.info("Apple server notification: {}", body);
public BaseResponse<Boolean> receiveNotification(@RequestBody Map<String, String> body, HttpServletRequest request) {
String signedPayload = body.get("signedPayload");
log.warn(body.toString());
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);
}
private AppleServerNotification decodeSignedPayload(String signedPayload) {
try {
JsonNode root = JwtParser.parsePayload(signedPayload);
AppleServerNotification notification = new AppleServerNotification();
notification.setNotificationType(text(root, "notificationType", "notification_type"));
JsonNode data = root.get("data");
if (data != null && !data.isNull()) {
notification.setEnvironment(text(data, "environment"));
notification.setProductId(text(data, "productId", "product_id"));
notification.setOriginalTransactionId(text(data, "originalTransactionId", "original_transaction_id"));
notification.setSignedTransactionInfo(text(data, "signedTransactionInfo", "signed_transaction_info"));
}
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")));
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());
} catch (Exception e) {
log.error("Failed to decode signedPayload", e);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "signedPayload 解析失败");
}
}
private String text(JsonNode node, String... keys) {
for (String k : keys) {
if (node != null && node.has(k) && !node.get(k).isNull()) {
return node.get(k).asText();
}
}
return null;
}
private String firstNonBlank(String a, String b) {
if (a != null && !a.isBlank()) {
return a;
}
return (b != null && !b.isBlank()) ? b : a;
}
private String epochToIso(JsonNode node, String... keys) {
String val = text(node, keys);
if (val == null) {
return null;
}
try {
long epochMillis = Long.parseLong(val);
return java.time.Instant.ofEpochMilli(epochMillis).toString();
} catch (NumberFormatException e) {
return val;
}
}
}