apple支付部分逻辑

This commit is contained in:
menxipeng
2025-11-06 23:26:02 +08:00
parent 5ac6929d2e
commit 3319343ca3
16 changed files with 737 additions and 494 deletions

View File

@@ -2,13 +2,16 @@ package com.ruoyi.web.controller.back;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.ruoyi.common.utils.http.HttpUtils; import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.system.util.AppleNotificationProcessor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
/** /**
* 描述: * 描述:
@@ -19,10 +22,23 @@ import java.util.Map;
@RestController @RestController
public class CallBackController { public class CallBackController {
private static final Logger log = LoggerFactory.getLogger(CallBackController.class); private static final Logger log = LoggerFactory.getLogger(CallBackController.class);
@Autowired
private AppleNotificationProcessor notificationProcessor;
@RequestMapping("/ios") @RequestMapping("/ios")
public void iosCallBack(@RequestBody Map<String,Object> param){ public void iosCallBack(@RequestBody String notificationBody) {
System.out.println(param); //System.out.println(notificationBody);
log.info("ios 回调参数:{}", JSONUtil.toJsonStr( param)); //log.info("ios 回调参数:{}", JSONUtil.toJsonStr(notificationBody));
try {
// 异步处理,避免超时
CompletableFuture.runAsync(() -> {
notificationProcessor.processSignedPayload(notificationBody);
});
}catch (Exception e){
log.error("处理iOS回调异常: {}", e.getMessage());
}
} }
} }

View File

@@ -40,7 +40,6 @@ public class ClientOrderInfoController extends BaseController
// 获取客户端IP // 获取客户端IP
String clientIp = getClientIp(httpRequest); String clientIp = getClientIp(httpRequest);
request.setClientIp(clientIp); request.setClientIp(clientIp);
return orderInfoService.createOrder(request); return orderInfoService.createOrder(request);
} catch (Exception e) { } catch (Exception e) {
return AjaxResult.error("创建订单失败: " + e.getMessage()); return AjaxResult.error("创建订单失败: " + e.getMessage());

View File

@@ -67,21 +67,21 @@ spring:
enabled: true enabled: true
# redis 配置 # redis 配置
redis: redis:
# host: 116.204.124.80 host: 116.204.124.80
# # 端口默认为6379
# port: 16379
# # 数据库索引
# database: 0
# # 密码
# password: Lwz19520416443@
# # 地址
host: 127.0.0.1
# 端口默认为6379 # 端口默认为6379
port: 6379 port: 16379
# 数据库索引 # 数据库索引
database: 0 database: 0
# 密码 # 密码
password: password: Lwz19520416443@
# # 地址
# host: 127.0.0.1
# # 端口默认为6379
# port: 6379
# # 数据库索引
# database: 0
# # 密码
# password:
# 连接超时时间 # 连接超时时间
timeout: 10s timeout: 10s
lettuce: lettuce:

View File

@@ -21,7 +21,7 @@ public class OrderCreateRequest implements Serializable {
/** 支付方式 aliPay/wechatPay/applePay */ /** 支付方式 aliPay/wechatPay/applePay */
private String payType; private String payType;
/** 套餐类型 1包月 3包季度 6半年 12一年 */ /** 套餐类型 1 包月 2 包季度 3 半年 4 一年 */
private String packageType; private String packageType;
/** 设备类型 android/ios */ /** 设备类型 android/ios */

View File

@@ -2,6 +2,7 @@ package com.ruoyi.common.core.domain.entity;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel; import com.ruoyi.common.annotation.Excel;
@@ -13,6 +14,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
* @author ruoyi * @author ruoyi
* @date 2025-08-03 * @date 2025-08-03
*/ */
@Data
public class OrderInfo extends BaseEntity public class OrderInfo extends BaseEntity
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -100,232 +102,4 @@ public class OrderInfo extends BaseEntity
/** 乐观锁版本号 */ /** 乐观锁版本号 */
@Excel(name = "乐观锁版本号") @Excel(name = "乐观锁版本号")
private Long version; private Long version;
public void setId(String id)
{
this.id = id;
}
public String getId()
{
return id;
}
public void setOrderId(Long orderId)
{
this.orderId = orderId;
}
public Long getOrderId()
{
return orderId;
}
public void setOrderName(String orderName)
{
this.orderName = orderName;
}
public String getOrderName()
{
return orderName;
}
public void setUserId(Long userId)
{
this.userId = userId;
}
public Long getUserId()
{
return userId;
}
public void setAmount(Long amount)
{
this.amount = amount;
}
public Long getAmount()
{
return amount;
}
public void setPayType(String payType)
{
this.payType = payType;
}
public String getPayType()
{
return payType;
}
public void setPayTime(Date payTime)
{
this.payTime = payTime;
}
public Date getPayTime()
{
return payTime;
}
public void setPayStatus(Long payStatus)
{
this.payStatus = payStatus;
}
public Long getPayStatus()
{
return payStatus;
}
public void setStartTime(Date startTime)
{
this.startTime = startTime;
}
public Date getStartTime()
{
return startTime;
}
public void setEndTime(Date endTime)
{
this.endTime = endTime;
}
public Date getEndTime()
{
return endTime;
}
public void setIdDel(Long idDel)
{
this.idDel = idDel;
}
public Long getIdDel()
{
return idDel;
}
public void setCallTime(Date callTime)
{
this.callTime = callTime;
}
public Date getCallTime()
{
return callTime;
}
public void setPackageType(String packageType)
{
this.packageType = packageType;
}
public String getPackageType()
{
return packageType;
}
public void setCallbackContent(String callbackContent)
{
this.callbackContent = callbackContent;
}
public String getCallbackContent()
{
return callbackContent;
}
public void setTradeNo(String tradeNo)
{
this.tradeNo = tradeNo;
}
public String getTradeNo()
{
return tradeNo;
}
public void setRefundAmount(Long refundAmount)
{
this.refundAmount = refundAmount;
}
public Long getRefundAmount()
{
return refundAmount;
}
public void setRefundTime(Date refundTime)
{
this.refundTime = refundTime;
}
public Date getRefundTime()
{
return refundTime;
}
public void setClientIp(String clientIp)
{
this.clientIp = clientIp;
}
public String getClientIp()
{
return clientIp;
}
public void setDeviceType(String deviceType)
{
this.deviceType = deviceType;
}
public String getDeviceType()
{
return deviceType;
}
public void setVersion(Long version)
{
this.version = version;
}
public Long getVersion()
{
return version;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("orderId", getOrderId())
.append("orderName", getOrderName())
.append("userId", getUserId())
.append("amount", getAmount())
.append("payType", getPayType())
.append("payTime", getPayTime())
.append("payStatus", getPayStatus())
.append("startTime", getStartTime())
.append("endTime", getEndTime())
.append("createTime", getCreateTime())
.append("idDel", getIdDel())
.append("updateTime", getUpdateTime())
.append("callTime", getCallTime())
.append("packageType", getPackageType())
.append("callbackContent", getCallbackContent())
.append("tradeNo", getTradeNo())
.append("refundAmount", getRefundAmount())
.append("refundTime", getRefundTime())
.append("clientIp", getClientIp())
.append("deviceType", getDeviceType())
.append("version", getVersion())
.toString();
}
} }

View File

@@ -3,6 +3,7 @@ package com.ruoyi.common.core.domain.entity;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel; import com.ruoyi.common.annotation.Excel;
@@ -14,6 +15,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
* @author ruoyi * @author ruoyi
* @date 2025-10-27 * @date 2025-10-27
*/ */
@Data
public class Product extends BaseEntity public class Product extends BaseEntity
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -35,7 +37,7 @@ public class Product extends BaseEntity
/** 分类ID */ /** 分类ID */
@Excel(name = "分类ID") @Excel(name = "分类ID")
private Long categoryId; private String categoryId;
/** 商品类型1-包月2-包季3-半年 */ /** 商品类型1-包月2-包季3-半年 */
@Excel(name = "商品类型1-包月2-包季3-半年 ") @Excel(name = "商品类型1-包月2-包季3-半年 ")
@@ -79,174 +81,4 @@ public class Product extends BaseEntity
@Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd")
private Date updatedAt; private Date updatedAt;
public void setId(String id)
{
this.id = id;
}
public String getId()
{
return id;
}
public void setProductId(Long productId)
{
this.productId = productId;
}
public Long getProductId()
{
return productId;
}
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
public void setDescription(String description)
{
this.description = description;
}
public String getDescription()
{
return description;
}
public void setCategoryId(Long categoryId)
{
this.categoryId = categoryId;
}
public Long getCategoryId()
{
return categoryId;
}
public void setProductType(Integer productType)
{
this.productType = productType;
}
public Integer getProductType()
{
return productType;
}
public void setStatus(Integer status)
{
this.status = status;
}
public Integer getStatus()
{
return status;
}
public void setOriginalPrice(BigDecimal originalPrice)
{
this.originalPrice = originalPrice;
}
public BigDecimal getOriginalPrice()
{
return originalPrice;
}
public void setCurrentPrice(BigDecimal currentPrice)
{
this.currentPrice = currentPrice;
}
public BigDecimal getCurrentPrice()
{
return currentPrice;
}
public void setMonthlyDuration(Long monthlyDuration)
{
this.monthlyDuration = monthlyDuration;
}
public Long getMonthlyDuration()
{
return monthlyDuration;
}
public void setQuarterlyDuration(Long quarterlyDuration)
{
this.quarterlyDuration = quarterlyDuration;
}
public Long getQuarterlyDuration()
{
return quarterlyDuration;
}
public void setStock(Long stock)
{
this.stock = stock;
}
public Long getStock()
{
return stock;
}
public void setSales(Long sales)
{
this.sales = sales;
}
public Long getSales()
{
return sales;
}
public void setCreatedAt(Date createdAt)
{
this.createdAt = createdAt;
}
public Date getCreatedAt()
{
return createdAt;
}
public void setUpdatedAt(Date updatedAt)
{
this.updatedAt = updatedAt;
}
public Date getUpdatedAt()
{
return updatedAt;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("productId", getProductId())
.append("name", getName())
.append("description", getDescription())
.append("categoryId", getCategoryId())
.append("productType", getProductType())
.append("status", getStatus())
.append("originalPrice", getOriginalPrice())
.append("currentPrice", getCurrentPrice())
.append("monthlyDuration", getMonthlyDuration())
.append("quarterlyDuration", getQuarterlyDuration())
.append("stock", getStock())
.append("sales", getSales())
.append("createdAt", getCreatedAt())
.append("updatedAt", getUpdatedAt())
.toString();
}
} }

View File

@@ -131,7 +131,8 @@ public class SecurityConfig
permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
// 对于登录login 注册register 验证码captchaImage 允许匿名访问 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
//"/client/**","/back/**" //"/client/**","/back/**"
requests.antMatchers("/login", "/register","/client/shopLogin","/file/download/**", "/captchaImage","/client/getCode","/back/**","/client/file/**").permitAll() requests.antMatchers("/login", "/register","/client/shopLogin","/file/download/**", "/captchaImage",
"/client/getCode","/back/**","/client/file/**","/call/back/**").permitAll()
// 静态资源,可匿名访问 // 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()

View File

@@ -23,6 +23,13 @@
<artifactId>ruoyi-common</artifactId> <artifactId>ruoyi-common</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -13,6 +13,7 @@ import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.mapper.OrderInfoMapper; import com.ruoyi.system.mapper.OrderInfoMapper;
import com.ruoyi.system.service.IOrderInfoService; import com.ruoyi.system.service.IOrderInfoService;
import com.ruoyi.system.util.AppleyPay;
import com.ruoyi.system.util.PaymentUtil; import com.ruoyi.system.util.PaymentUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -174,15 +175,20 @@ public class OrderInfoServiceImpl implements IOrderInfoService
// 检查用户权限 // 检查用户权限
LoginUser loginUser = SecurityUtils.getLoginUser(); LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null || !loginUser.getUserId().equals(orderInfo.getUserId())) { // if (loginUser == null || !loginUser.getUserId().equals(orderInfo.getUserId())) {
return AjaxResult.error("无权限操作此订单"); // return AjaxResult.error("无权限操作此订单");
// }
// 调用第三方支付接口 TODO// 待修改
processPayment(orderInfo, request);
boolean paymentResult = true;
if (paymentResult){
return AjaxResult.success("支付请求成功");
}else {
return AjaxResult.error("支付请求失败");
} }
// 调用第三方支付接口
Map<String, Object> paymentResult = processPayment(orderInfo, request);
return AjaxResult.success("支付请求成功", paymentResult);
} catch (Exception e) { } catch (Exception e) {
return AjaxResult.error("支付异常: " + e.getMessage()); return AjaxResult.error("支付异常: " + e.getMessage());
} }
@@ -336,22 +342,29 @@ public class OrderInfoServiceImpl implements IOrderInfoService
} }
} }
@Autowired
private AppleyPay appleyPay;
/** /**
* 处理支付过程 * 处理支付过程
*/ */
private Map<String, Object> processPayment(OrderInfo orderInfo, PaymentRequest request) { private boolean processPayment(OrderInfo orderInfo, PaymentRequest request) {
String payType = orderInfo.getPayType();
// 根据支付方式调用相应的支付接口 // 根据支付方式调用相应的支付接口
switch (request.getPayType()) { switch (payType) {
case "aliPay": case "aliPay":
return paymentUtil.processAliPay(orderInfo, request); //return paymentUtil.processAliPay(orderInfo, request);
return false;
case "wechatPay": case "wechatPay":
return paymentUtil.processWechatPay(orderInfo, request); //return paymentUtil.processWechatPay(orderInfo, request);
return false;
case "applePay": case "applePay":
return paymentUtil.processApplePay(orderInfo, request); return appleyPay.setIapCertificate(String.valueOf(orderInfo.getUserId()), request.getPaymentToken(), false);
default: default:
Map<String, Object> errorResult = new HashMap<>(); Map<String, Object> errorResult = new HashMap<>();
errorResult.put("error", "不支持的支付方式"); errorResult.put("error", "不支持的支付方式");
return errorResult; return false;
} }
} }

View File

@@ -0,0 +1,226 @@
package com.ruoyi.system.util;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class AppleNotificationProcessor {
@Autowired
private AppleSignedPayloadDecoder payloadDecoder;
/**
* 完整的通知处理流程
*/
public void processSignedPayload(String signedPayload) {
try {
// 1. 解码
JSONObject payload = payloadDecoder.decodeSignedPayload(signedPayload);
if (payload == null) {
log.error("Payload解码失败");
return;
}
// 2. 提取各种信息
NotificationInfo notificationInfo = payloadDecoder.extractNotificationInfo(payload);
TransactionInfo transactionInfo = payloadDecoder.extractTransactionInfo(notificationInfo.getData());
RenewalInfo renewalInfo = payloadDecoder.extractRenewalInfo(notificationInfo.getData());
// 3. 记录日志
logNotificationDetails(notificationInfo, transactionInfo, renewalInfo);
// 4. 业务处理
processBusinessLogic(notificationInfo, transactionInfo, renewalInfo);
} catch (Exception e) {
log.error("处理通知完整流程异常: {}", e.getMessage(), e);
}
}
/**
* 记录详细的通知信息
*/
private void logNotificationDetails(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
log.info("=== 苹果通知详情 ===");
log.info("通知类型: {}, 子类型: {}",
notificationInfo.getNotificationType(),
notificationInfo.getSubtype());
log.info("通知UUID: {}, 版本: {}",
notificationInfo.getNotificationUUID(),
notificationInfo.getVersion());
if (transactionInfo != null) {
log.info("交易信息 - 原始ID: {}, 产品: {}",
transactionInfo.getOriginalTransactionId(),
transactionInfo.getProductId());
log.info("购买时间: {}, 过期时间: {}",
transactionInfo.getPurchaseDate(),
transactionInfo.getExpiresDate());
}
if (renewalInfo != null) {
log.info("续订信息 - 自动续订状态: {}, 续订产品: {}",
renewalInfo.getAutoRenewStatus(),
renewalInfo.getAutoRenewProductId());
}
log.info("=== 通知详情结束 ===");
}
/**
* 业务逻辑处理
*/
private void processBusinessLogic(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
String notificationType = notificationInfo.getNotificationType();
String originalTransactionId = getOriginalTransactionId(transactionInfo, renewalInfo);
// 根据通知类型路由到不同的处理方法
switch (notificationType) {
case "SUBSCRIBED":
handleSubscriptionEvent(notificationInfo, transactionInfo, renewalInfo);
break;
case "DID_RENEW":
handleRenewalEvent(notificationInfo, transactionInfo, renewalInfo);
break;
case "DID_FAIL_TO_RENEW":
handleRenewalFailureEvent(notificationInfo, transactionInfo, renewalInfo);
break;
case "DID_CHANGE_RENEWAL_PREF":
handleRenewalPreferenceChange(notificationInfo, transactionInfo, renewalInfo);
break;
case "DID_CHANGE_RENEWAL_STATUS":
handleRenewalStatusChange(notificationInfo, transactionInfo, renewalInfo);
break;
case "OFFER_REDEEMED":
handleOfferRedeemed(notificationInfo, transactionInfo, renewalInfo);
break;
case "EXPIRED":
handleExpiredEvent(notificationInfo, transactionInfo, renewalInfo);
break;
case "REFUND":
handleRefundEvent(notificationInfo, transactionInfo, renewalInfo);
break;
case "REVOKE":
handleRevokeEvent(notificationInfo, transactionInfo, renewalInfo);
break;
case "GRACE_PERIOD_EXPIRED":
handleGracePeriodExpired(notificationInfo, transactionInfo, renewalInfo);
break;
default:
log.warn("未处理的通知类型: {}", notificationType);
}
}
private String getOriginalTransactionId(TransactionInfo transactionInfo, RenewalInfo renewalInfo) {
if (transactionInfo != null && StringUtils.isNotEmpty(transactionInfo.getOriginalTransactionId())) {
return transactionInfo.getOriginalTransactionId();
}
if (renewalInfo != null && StringUtils.isNotEmpty(renewalInfo.getOriginalTransactionId())) {
return renewalInfo.getOriginalTransactionId();
}
return "Unknown";
}
// 具体的业务处理方法
private void handleSubscriptionEvent(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
log.info("处理订阅事件 - 子类型: {}", notificationInfo.getSubtype());
if (transactionInfo != null) {
}
}
private void handleRenewalEvent(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
log.info("处理续订事件 - 子类型: {}", notificationInfo.getSubtype());
if (transactionInfo != null) {
}
}
private void handleRenewalFailureEvent(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
log.warn("处理续订失败事件 - 子类型: {}", notificationInfo.getSubtype());
String transactionId = getOriginalTransactionId(transactionInfo, renewalInfo);
}
private void handleRenewalPreferenceChange(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
log.info("处理续订偏好变更 - 子类型: {}", notificationInfo.getSubtype());
if (renewalInfo != null) {
}
}
private void handleRenewalStatusChange(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
log.info("处理续订状态变更 - 子类型: {}", notificationInfo.getSubtype());
if (renewalInfo != null) {
}
}
private void handleOfferRedeemed(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
log.info("处理优惠兑换 - 子类型: {}", notificationInfo.getSubtype());
// TODO: 处理优惠兑换逻辑
}
private void handleExpiredEvent(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
log.warn("处理过期事件 - 子类型: {}", notificationInfo.getSubtype());
String transactionId = getOriginalTransactionId(transactionInfo, renewalInfo);
}
private void handleRefundEvent(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
log.warn("处理退款事件 - 子类型: {}", notificationInfo.getSubtype());
String transactionId = getOriginalTransactionId(transactionInfo, renewalInfo);
}
private void handleRevokeEvent(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
log.warn("处理权益撤销事件 - 子类型: {}", notificationInfo.getSubtype());
String transactionId = getOriginalTransactionId(transactionInfo, renewalInfo);
}
private void handleGracePeriodExpired(NotificationInfo notificationInfo,
TransactionInfo transactionInfo,
RenewalInfo renewalInfo) {
log.warn("处理宽限期过期事件 - 子类型: {}", notificationInfo.getSubtype());
String transactionId = getOriginalTransactionId(transactionInfo, renewalInfo);
}
}

View File

@@ -0,0 +1,152 @@
package com.ruoyi.system.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@Service
public class AppleSignedPayloadDecoder {
private static final Logger log = LoggerFactory.getLogger(AppleSignedPayloadDecoder.class);
/**
* 直接解码Signed Payload无需公钥验证
*/
public JSONObject decodeSignedPayload(String signedPayload) {
try {
log.info("开始解码Signed Payload");
// 1. 直接解码JWT不验证签名
String[] parts = signedPayload.split("\\.");
if (parts.length != 3) {
log.error("Invalid JWT format");
return null;
}
// 2. 解码Payload部分
String payloadJson = decodeBase64Url(parts[1]);
JSONObject payload = JSON.parseObject(payloadJson);
log.info("解码成功 - 通知类型: {}", payload.getString("notificationType"));
return payload;
} catch (Exception e) {
log.error("解码Signed Payload异常: {}", e.getMessage(), e);
return null;
}
}
/**
* Base64URL解码
*/
private String decodeBase64Url(String base64Url) {
try {
// 将Base64URL转换为标准Base64
String base64 = base64Url.replace('-', '+').replace('_', '/');
// 添加padding
switch (base64.length() % 4) {
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}
byte[] decodedBytes = Base64.getDecoder().decode(base64);
return new String(decodedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("Base64URL解码失败: {}", e.getMessage());
throw new RuntimeException("Base64解码失败", e);
}
}
/**
* 提取关键通知信息
*/
public NotificationInfo extractNotificationInfo(JSONObject payload) {
NotificationInfo info = new NotificationInfo();
info.setNotificationType(payload.getString("notificationType"));
info.setSubtype(payload.getString("subtype"));
info.setNotificationUUID(payload.getString("notificationUUID"));
info.setVersion(payload.getString("version"));
info.setData(payload.getJSONObject("data"));
return info;
}
/**
* 从data中提取交易信息
*/
public TransactionInfo extractTransactionInfo(JSONObject data) {
try {
if (data == null) return null;
String signedTransactionInfo = data.getString("signedTransactionInfo");
if (StringUtils.isEmpty(signedTransactionInfo)) {
return null;
}
// 解码交易信息JWT
String[] transactionParts = signedTransactionInfo.split("\\.");
if (transactionParts.length != 3) return null;
String transactionPayload = decodeBase64Url(transactionParts[1]);
JSONObject transactionJson = JSON.parseObject(transactionPayload);
TransactionInfo transactionInfo = new TransactionInfo();
transactionInfo.setOriginalTransactionId(transactionJson.getString("originalTransactionId"));
transactionInfo.setTransactionId(transactionJson.getString("transactionId"));
transactionInfo.setProductId(transactionJson.getString("productId"));
transactionInfo.setPurchaseDate(transactionJson.getString("purchaseDate"));
transactionInfo.setExpiresDate(transactionJson.getString("expiresDate"));
transactionInfo.setOriginalPurchaseDate(transactionJson.getString("originalPurchaseDate"));
return transactionInfo;
} catch (Exception e) {
log.error("提取交易信息异常: {}", e.getMessage());
return null;
}
}
/**
* 从data中提取续订信息
*/
public RenewalInfo extractRenewalInfo(JSONObject data) {
try {
if (data == null) return null;
String signedRenewalInfo = data.getString("signedRenewalInfo");
if (StringUtils.isEmpty(signedRenewalInfo)) {
return null;
}
// 解码续订信息JWT
String[] renewalParts = signedRenewalInfo.split("\\.");
if (renewalParts.length != 3) return null;
String renewalPayload = decodeBase64Url(renewalParts[1]);
JSONObject renewalJson = JSON.parseObject(renewalPayload);
RenewalInfo renewalInfo = new RenewalInfo();
renewalInfo.setOriginalTransactionId(renewalJson.getString("originalTransactionId"));
renewalInfo.setAutoRenewStatus(renewalJson.getString("autoRenewStatus"));
renewalInfo.setProductId(renewalJson.getString("productId"));
renewalInfo.setAutoRenewProductId(renewalJson.getString("autoRenewProductId"));
return renewalInfo;
} catch (Exception e) {
log.error("提取续订信息异常: {}", e.getMessage());
return null;
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,108 @@
package com.ruoyi.system.util;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 描述:
*
* @author MXP by 2025/11/6
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InAppTransaction {
/**
* 购买数量
*/
private String quantity;
/**
* 产品ID
*/
private String productId;
/**
* 交易ID
*/
private String transactionId;
/**
* 原始交易ID对于订阅整个订阅周期保持不变
*/
private String originalTransactionId;
/**
* 购买日期(字符串格式)
*/
private String purchaseDate;
/**
* 购买日期(时间戳毫秒)
*/
private String purchaseDateMs;
/**
* 购买日期(太平洋时间)
*/
private String purchaseDatePst;
/**
* 原始购买日期(字符串格式)
*/
private String originalPurchaseDate;
/**
* 原始购买日期(时间戳毫秒)
*/
private String originalPurchaseDateMs;
/**
* 原始购买日期(太平洋时间)
*/
private String originalPurchaseDatePst;
/**
* 过期日期(字符串格式)- 仅订阅有效
*/
private String expiresDate;
/**
* 过期日期(时间戳毫秒)- 仅订阅有效
*/
private String expiresDateMs;
/**
* 过期日期(太平洋时间)- 仅订阅有效
*/
private String expiresDatePst;
/**
* Web订单行项目ID
*/
private String webOrderLineItemId;
/**
* 是否试用期
*/
private String isTrialPeriod;
/**
* 是否在介绍优惠期
*/
private String isInIntroOfferPeriod;
/**
* 应用内购买所有权类型
*/
private String inAppOwnershipType;
/**
* 订阅组标识符
*/
private String subscriptionGroupIdentifier;
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.system.util;
/**
* 描述:
*
* @author MXP by 2025/11/6
*/
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
/**
* 通知信息DTO
*/
@Data
class NotificationInfo {
private String notificationType;
private String subtype;
private String notificationUUID;
private String version;
private JSONObject data;
}

View File

@@ -0,0 +1,20 @@
package com.ruoyi.system.util;
/**
* 描述:
*
* @author MXP by 2025/11/6
*/
import lombok.Data;
/**
* 续订信息DTO
*/
@Data
class RenewalInfo {
private String originalTransactionId;
private String autoRenewStatus;
private String productId;
private String autoRenewProductId;
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.system.util;
/**
* 描述:
*
* @author MXP by 2025/11/6
*/
import lombok.Data;
/**
* 交易信息DTO
*/
@Data
class TransactionInfo {
private String originalTransactionId;
private String transactionId;
private String productId;
private String purchaseDate;
private String expiresDate;
private String originalPurchaseDate;
}