feat(apple): 新增服务器通知续订与JWT解析能力
- 支持解析Apple签名JWT并提取交易信息 - 新增processRenewNotification处理续订通知 - 添加测试用JWT生成、解析及发送重试记录示例 - 移除废弃ApplePayUtil,统一走新验证逻辑
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user