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

View File

@@ -19,7 +19,7 @@ import java.util.List;
/**
* 客户端订单Controller
*
*
* @author ruoyi
* @date 2025-01-27
*/
@@ -40,7 +40,6 @@ public class ClientOrderInfoController extends BaseController
// 获取客户端IP
String clientIp = getClientIp(httpRequest);
request.setClientIp(clientIp);
return orderInfoService.createOrder(request);
} catch (Exception e) {
return AjaxResult.error("创建订单失败: " + e.getMessage());
@@ -110,7 +109,7 @@ public class ClientOrderInfoController extends BaseController
if (loginUser == null) {
return AjaxResult.error(401,"用户未登录");
}
List<OrderInfo> orders = orderInfoService.selectUserOrders(loginUser.getUserId());
return AjaxResult.success("查询成功", orders);
} catch (Exception e) {
@@ -128,13 +127,13 @@ public class ClientOrderInfoController extends BaseController
if (orderInfo == null) {
return AjaxResult.error("订单不存在");
}
// 检查用户权限
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null || !loginUser.getUserId().equals(orderInfo.getUserId())) {
return AjaxResult.error(401,"无权限查看此订单");
}
return AjaxResult.success("查询成功", orderInfo);
} catch (Exception e) {
return AjaxResult.error("查询订单详情失败: " + e.getMessage());

View File

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

View File

@@ -4,29 +4,29 @@ import java.io.Serializable;
/**
* 订单创建请求DTO
*
*
* @author ruoyi
* @date 2025-01-27
*/
public class OrderCreateRequest implements Serializable {
private static final long serialVersionUID = 1L;
/** 订单名称 */
private String orderName;
/** 金额(分) */
private Long amount;
/** 支付方式 aliPay/wechatPay/applePay */
private String payType;
/** 套餐类型 1包月 3包季度 6半年 12一年 */
/** 套餐类型 1 包月 2 包季度 3 半年 4 一年 */
private String packageType;
/** 设备类型 android/ios */
private String deviceType;
/** 客户端IP */
private String clientIp;
@@ -77,4 +77,4 @@ public class OrderCreateRequest implements Serializable {
public void setClientIp(String clientIp) {
this.clientIp = clientIp;
}
}
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.common.core.domain.entity;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
@@ -9,10 +10,11 @@ import com.ruoyi.common.core.domain.BaseEntity;
/**
* 【请填写功能名称】对象 order_info
*
*
* @author ruoyi
* @date 2025-08-03
*/
@Data
public class OrderInfo extends BaseEntity
{
private static final long serialVersionUID = 1L;
@@ -100,232 +102,4 @@ public class OrderInfo extends BaseEntity
/** 乐观锁版本号 */
@Excel(name = "乐观锁版本号")
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.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
@@ -14,6 +15,7 @@ import com.ruoyi.common.core.domain.BaseEntity;
* @author ruoyi
* @date 2025-10-27
*/
@Data
public class Product extends BaseEntity
{
private static final long serialVersionUID = 1L;
@@ -35,7 +37,7 @@ public class Product extends BaseEntity
/** 分类ID */
@Excel(name = "分类ID")
private Long categoryId;
private String categoryId;
/** 商品类型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")
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

@@ -26,7 +26,7 @@ import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
/**
* spring security配置
*
*
* @author ruoyi
*/
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@@ -44,7 +44,7 @@ public class SecurityConfig
@Qualifier("shopUserDetailServiceImpl")
private UserDetailsService shopUserDetailService;
/**
* 认证失败处理类
*/
@@ -62,7 +62,7 @@ public class SecurityConfig
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@@ -131,7 +131,8 @@ public class SecurityConfig
permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
//"/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("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()

View File

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

View File

@@ -13,6 +13,7 @@ import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.mapper.OrderInfoMapper;
import com.ruoyi.system.service.IOrderInfoService;
import com.ruoyi.system.util.AppleyPay;
import com.ruoyi.system.util.PaymentUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -22,22 +23,22 @@ import java.util.*;
/**
* 【请填写功能名称】Service业务层处理
*
*
* @author ruoyi
* @date 2025-08-03
*/
@Service
public class OrderInfoServiceImpl implements IOrderInfoService
public class OrderInfoServiceImpl implements IOrderInfoService
{
@Autowired
private OrderInfoMapper orderInfoMapper;
@Autowired
private PaymentUtil paymentUtil;
/**
* 查询【请填写功能名称】
*
*
* @param id 【请填写功能名称】主键
* @return 【请填写功能名称】
*/
@@ -49,7 +50,7 @@ public class OrderInfoServiceImpl implements IOrderInfoService
/**
* 查询【请填写功能名称】列表
*
*
* @param orderInfo 【请填写功能名称】
* @return 【请填写功能名称】
*/
@@ -61,7 +62,7 @@ public class OrderInfoServiceImpl implements IOrderInfoService
/**
* 新增【请填写功能名称】
*
*
* @param orderInfo 【请填写功能名称】
* @return 结果
*/
@@ -74,7 +75,7 @@ public class OrderInfoServiceImpl implements IOrderInfoService
/**
* 修改【请填写功能名称】
*
*
* @param orderInfo 【请填写功能名称】
* @return 结果
*/
@@ -87,7 +88,7 @@ public class OrderInfoServiceImpl implements IOrderInfoService
/**
* 批量删除【请填写功能名称】
*
*
* @param ids 需要删除的【请填写功能名称】主键
* @return 结果
*/
@@ -99,7 +100,7 @@ public class OrderInfoServiceImpl implements IOrderInfoService
/**
* 删除【请填写功能名称】信息
*
*
* @param id 【请填写功能名称】主键
* @return 结果
*/
@@ -117,13 +118,13 @@ public class OrderInfoServiceImpl implements IOrderInfoService
if (StringUtils.isEmpty(request.getOrderName()) || request.getAmount() == null || request.getAmount() <= 0) {
return AjaxResult.error("订单信息不完整");
}
// 获取当前用户
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null) {
return AjaxResult.error(401,"用户未登录");
}
// 创建订单
OrderInfo orderInfo = new OrderInfo();
orderInfo.setId(UUID.randomUUID().toString().replace("-", ""));
@@ -139,7 +140,7 @@ public class OrderInfoServiceImpl implements IOrderInfoService
orderInfo.setIdDel(0L);
orderInfo.setVersion(1L);
orderInfo.setCreateTime(DateUtils.getNowDate());
// 保存订单
int result = insertOrderInfo(orderInfo);
if (result > 0) {
@@ -160,29 +161,34 @@ public class OrderInfoServiceImpl implements IOrderInfoService
if (StringUtils.isEmpty(request.getOrderId())) {
return AjaxResult.error("订单ID不能为空");
}
// 查询订单
OrderInfo orderInfo = selectOrderByOrderId(request.getOrderId());
if (orderInfo == null) {
return AjaxResult.error("订单不存在");
}
// 检查订单状态
if (orderInfo.getPayStatus() != PayStatusEnum.CREATE.getStatus()) {
return AjaxResult.error("订单状态不正确");
}
// 检查用户权限
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null || !loginUser.getUserId().equals(orderInfo.getUserId())) {
return AjaxResult.error("无权限操作此订单");
// if (loginUser == null || !loginUser.getUserId().equals(orderInfo.getUserId())) {
// 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) {
return AjaxResult.error("支付异常: " + e.getMessage());
}
@@ -197,12 +203,12 @@ public class OrderInfoServiceImpl implements IOrderInfoService
if (orderInfo == null) {
return AjaxResult.error("订单不存在");
}
// 检查订单状态
if (orderInfo.getPayStatus() != PayStatusEnum.CREATE.getStatus()) {
return AjaxResult.error("订单状态不正确");
}
// 更新订单状态
orderInfo.setPayStatus(PayStatusEnum.PENDING.getStatus()); // 2-待出货
orderInfo.setPayTime(DateUtils.getNowDate());
@@ -210,10 +216,10 @@ public class OrderInfoServiceImpl implements IOrderInfoService
orderInfo.setCallTime(DateUtils.getNowDate());
orderInfo.setCallbackContent("支付成功回调");
orderInfo.setUpdateTime(DateUtils.getNowDate());
// 计算服务时间
calculateServiceTime(orderInfo);
// 保存订单
int result = updateOrderInfo(orderInfo);
if (result > 0) {
@@ -221,7 +227,7 @@ public class OrderInfoServiceImpl implements IOrderInfoService
} else {
return AjaxResult.error("支付完成处理失败");
}
} catch (Exception e) {
return AjaxResult.error("支付完成处理异常: " + e.getMessage());
}
@@ -235,44 +241,44 @@ public class OrderInfoServiceImpl implements IOrderInfoService
if (StringUtils.isEmpty(request.getOrderId()) || request.getRefundAmount() == null || request.getRefundAmount() <= 0) {
return AjaxResult.error("退款信息不完整");
}
// 查询订单
OrderInfo orderInfo = selectOrderByOrderId(request.getOrderId());
if (orderInfo == null) {
return AjaxResult.error("订单不存在");
}
// 检查订单状态
if (orderInfo.getPayStatus() != PayStatusEnum.PENDING.getStatus() && orderInfo.getPayStatus() != PayStatusEnum.WAIT_REFUND.getStatus()) {
return AjaxResult.error("订单状态不允许退款");
}
// 检查退款金额
if (request.getRefundAmount() > orderInfo.getAmount()) {
return AjaxResult.error("退款金额不能大于订单金额");
}
// 检查是否已退款
if (orderInfo.getRefundAmount() != null && orderInfo.getRefundAmount() > 0) {
return AjaxResult.error("订单已退款");
}
// 调用第三方退款接口
boolean refundResult = paymentUtil.processRefund(orderInfo.getOrderId().toString(), request.getRefundAmount(), orderInfo.getPayType());
if (!refundResult) {
return AjaxResult.error("第三方退款失败");
}
// 更新订单退款信息
orderInfo.setRefundAmount(request.getRefundAmount());
orderInfo.setRefundTime(DateUtils.getNowDate());
orderInfo.setUpdateTime(DateUtils.getNowDate());
// 如果是全额退款,更新订单状态
if (request.getRefundAmount().equals(orderInfo.getAmount())) {
orderInfo.setPayStatus(PayStatusEnum.REFUND.getStatus()); // 4-已退款
}
// 保存订单
int result = updateOrderInfo(orderInfo);
if (result > 0) {
@@ -280,7 +286,7 @@ public class OrderInfoServiceImpl implements IOrderInfoService
} else {
return AjaxResult.error("退款申请失败");
}
} catch (Exception e) {
return AjaxResult.error("退款异常: " + e.getMessage());
}
@@ -313,16 +319,16 @@ public class OrderInfoServiceImpl implements IOrderInfoService
if (orderInfo == null) {
return AjaxResult.error("订单不存在");
}
// 检查订单状态
if (orderInfo.getPayStatus() != PayStatusEnum.PENDING.getStatus()) {
return AjaxResult.error("订单状态不正确,只有已支付的订单才能确认出货");
}
// 更新订单状态为已完成
orderInfo.setPayStatus(PayStatusEnum.COMPLETE.getStatus()); // 3-已完成
orderInfo.setUpdateTime(DateUtils.getNowDate());
// 保存订单
int result = updateOrderInfo(orderInfo);
if (result > 0) {
@@ -330,28 +336,35 @@ public class OrderInfoServiceImpl implements IOrderInfoService
} else {
return AjaxResult.error("确认出货失败");
}
} catch (Exception e) {
return AjaxResult.error("确认出货异常: " + e.getMessage());
}
}
@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":
return paymentUtil.processAliPay(orderInfo, request);
//return paymentUtil.processAliPay(orderInfo, request);
return false;
case "wechatPay":
return paymentUtil.processWechatPay(orderInfo, request);
//return paymentUtil.processWechatPay(orderInfo, request);
return false;
case "applePay":
return paymentUtil.processApplePay(orderInfo, request);
return appleyPay.setIapCertificate(String.valueOf(orderInfo.getUserId()), request.getPaymentToken(), false);
default:
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("error", "不支持的支付方式");
return errorResult;
return false;
}
}
@@ -361,7 +374,7 @@ public class OrderInfoServiceImpl implements IOrderInfoService
private void calculateServiceTime(OrderInfo orderInfo) {
Date now = DateUtils.getNowDate();
orderInfo.setStartTime(now);
// 根据套餐类型计算结束时间
String packageType = orderInfo.getPackageType();
if ("1".equals(packageType)) {

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;
}