From 3319343ca38c0749a1aed5b7786a69c9fb21ffc7 Mon Sep 17 00:00:00 2001 From: menxipeng Date: Thu, 6 Nov 2025 23:26:02 +0800 Subject: [PATCH] =?UTF-8?q?apple=E6=94=AF=E4=BB=98=E9=83=A8=E5=88=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/back/CallBackController.java | 22 +- .../client/ClientOrderInfoController.java | 9 +- .../src/main/resources/application.yml | 22 +- .../core/domain/dto/OrderCreateRequest.java | 20 +- .../common/core/domain/entity/OrderInfo.java | 232 +----------------- .../common/core/domain/entity/Product.java | 174 +------------ .../framework/config/SecurityConfig.java | 9 +- ruoyi-system/pom.xml | 9 +- .../service/impl/OrderInfoServiceImpl.java | 109 ++++---- .../util/AppleNotificationProcessor.java | 226 +++++++++++++++++ .../util/AppleSignedPayloadDecoder.java | 152 ++++++++++++ .../java/com/ruoyi/system/util/AppleyPay.java | 75 +++++- .../ruoyi/system/util/InAppTransaction.java | 108 ++++++++ .../ruoyi/system/util/NotificationInfo.java | 22 ++ .../com/ruoyi/system/util/RenewalInfo.java | 20 ++ .../ruoyi/system/util/TransactionInfo.java | 22 ++ 16 files changed, 737 insertions(+), 494 deletions(-) create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/util/AppleNotificationProcessor.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/util/AppleSignedPayloadDecoder.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/util/InAppTransaction.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/util/NotificationInfo.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/util/RenewalInfo.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/util/TransactionInfo.java diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/back/CallBackController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/back/CallBackController.java index c96c90e..dfa9a25 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/back/CallBackController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/back/CallBackController.java @@ -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 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()); + } } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/client/ClientOrderInfoController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/client/ClientOrderInfoController.java index 5b391c7..15e943d 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/client/ClientOrderInfoController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/client/ClientOrderInfoController.java @@ -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 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()); diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 8b04d5a..c5fb20d 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -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: diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/dto/OrderCreateRequest.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/dto/OrderCreateRequest.java index 7f9cb30..b375f97 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/dto/OrderCreateRequest.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/dto/OrderCreateRequest.java @@ -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; } -} \ No newline at end of file +} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/OrderInfo.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/OrderInfo.java index c525a94..68ef1ef 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/OrderInfo.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/OrderInfo.java @@ -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(); - } } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/Product.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/Product.java index cea6089..f7e50b1 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/Product.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/Product.java @@ -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(); - } } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java index 3e7e49d..dde69e5 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -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() diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml index f514d3d..077672c 100644 --- a/ruoyi-system/pom.xml +++ b/ruoyi-system/pom.xml @@ -23,6 +23,13 @@ ruoyi-common + + org.projectlombok + lombok + 1.18.38 + provided + + - \ No newline at end of file + diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/OrderInfoServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/OrderInfoServiceImpl.java index 007abdb..e411d8c 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/OrderInfoServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/OrderInfoServiceImpl.java @@ -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 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 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 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)) { diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/util/AppleNotificationProcessor.java b/ruoyi-system/src/main/java/com/ruoyi/system/util/AppleNotificationProcessor.java new file mode 100644 index 0000000..65fc6ba --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/util/AppleNotificationProcessor.java @@ -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); + + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/util/AppleSignedPayloadDecoder.java b/ruoyi-system/src/main/java/com/ruoyi/system/util/AppleSignedPayloadDecoder.java new file mode 100644 index 0000000..bce7b4a --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/util/AppleSignedPayloadDecoder.java @@ -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; + } + } +} + + + + diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/util/AppleyPay.java b/ruoyi-system/src/main/java/com/ruoyi/system/util/AppleyPay.java index 58cce50..a3532bd 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/util/AppleyPay.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/util/AppleyPay.java @@ -3,6 +3,7 @@ package com.ruoyi.system.util; import cn.hutool.core.codec.Base64; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.ruoyi.common.utils.StringUtils; import org.junit.Test; @@ -18,10 +19,11 @@ import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.HashMap; - +import java.util.List; /** * 描述: @@ -64,23 +66,20 @@ public class AppleyPay { * @param receipt 苹果传递前端给的值 * @param chooseEnv 是否时测试环境 */ - public String setIapCertificate(String userId, String receipt, boolean chooseEnv) { + public boolean setIapCertificate(String userId, String receipt, boolean chooseEnv) { log.info("IOS端发送的购买凭证。数据有 userId = {},receipt = {},chooseEnv = {}",userId,receipt,chooseEnv); if (StringUtils.isEmpty(userId) || StringUtils.isEmpty(receipt)) { - return "用户ID 或者 receipt为空"; + log.error("用户ID 或者 receipt为空"); + return false; } String url = null; url = chooseEnv == true ? certificateUrl : certificateUrlTest; final String certificateCode = receipt; if (StringUtils.isNotEmpty(certificateCode)) { String s = sendHttpsCoon(url, certificateCode, userId); - if ("支付成功".equals(s)) { - return s; - } else { - return s; - } + return "支付成功".equals(s); } else { - return "receipt 为空!"; + return false; } } @@ -117,7 +116,7 @@ public class AppleyPay { JSONObject obj = new JSONObject(); obj.put("receipt-data", code); // 添加共享密钥 - 这里需要补充 - obj.put("password", "你的共享密钥"); + obj.put("password", "96f11cd1f6714d1fb4206fcad92854bf"); BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream()); buffOutStr.write(obj.toString().getBytes()); @@ -139,10 +138,15 @@ public class AppleyPay { } // 将一个Json对象转成一个HashMap JSONObject alljsoncode = JSON.parseObject(sb.toString()); - Object receipt = alljsoncode.get("receipt"); + Object receipt = alljsoncode.get("latest_receipt_info"); + // receipt 转成LinkList + List inAppTransactions = JSONUtil.toList(receipt.toString(), InAppTransaction.class); + HashMap hashMap = JSONUtil.toBean(receipt.toString(), HashMap.class); // 苹果给的订单号 String original_transaction_id = (String) hashMap.get("original_transaction_id"); + // 唯一ID + String transactionId = (String) hashMap.get("transaction_id"); //TODO 存储订单ID,并检查此订单ID是否存在,如果存在就证明已经发货了(避免二次发货) if ("com.mingyue.product.month".equals(hashMap.get("product_id"))) { //TODO 月付 @@ -179,7 +183,54 @@ public class AppleyPay { public static void main(String[] args) { AppleyPay appleyPay = new AppleyPay(); - appleyPay.jiemi(); + appleyPay.decodeSignedPayload("eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTVRDQ0E3YWdBd0lCQWdJUVI4S0h6ZG41NTRaL1VvcmFkTng5dHpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJMU1Ea3hPVEU1TkRRMU1Wb1hEVEkzTVRBeE16RTNORGN5TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCTm5WdmhjdjdpVCs3RXg1dEJNQmdyUXNwSHpJc1hSaTBZeGZlazdsdjh3RW1qL2JIaVd0TndKcWMyQm9IenNRaUVqUDdLRklJS2c0WTh5MC9ueW51QW1qZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRklGaW9HNHdNTVZBMWt1OXpKbUdOUEFWbjNlcU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TnBBREJtQWpFQStxWG5SRUM3aFhJV1ZMc0x4em5qUnBJelBmN1ZIejlWL0NUbTgrTEpsclFlcG5tY1B2R0xOY1g2WFBubGNnTEFBakVBNUlqTlpLZ2c1cFE3OWtuRjRJYlRYZEt2OHZ1dElETVhEbWpQVlQzZEd2RnRzR1J3WE95d1Iya1pDZFNyZmVvdCIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJub3RpZmljYXRpb25UeXBlIjoiRElEX1JFTkVXIiwibm90aWZpY2F0aW9uVVVJRCI6IjhjYTQ2MWEzLTA1MmItNDBhZi04NTkyLWQxMTk4ZmM1ODljNiIsImRhdGEiOnsiYXBwQXBwbGVJZCI6Njc1NDYwNjI1NSwiYnVuZGxlSWQiOiJjb20ubWluZ3l1ZS5saW5neGkiLCJidW5kbGVWZXJzaW9uIjoiMSIsImVudmlyb25tZW50IjoiU2FuZGJveCIsInNpZ25lZFRyYW5zYWN0aW9uSW5mbyI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5nMVl5STZXeUpOU1VsRlRWUkRRMEUzWVdkQmQwbENRV2RKVVZJNFMwaDZaRzQxTlRSYUwxVnZjbUZrVG5nNWRIcEJTMEpuWjNGb2EycFBVRkZSUkVGNlFqRk5WVkYzVVdkWlJGWlJVVVJFUkhSQ1kwaENjMXBUUWxoaU0wcHpXa2hrY0ZwSFZXZFNSMVl5V2xkNGRtTkhWbmxKUmtwc1lrZEdNR0ZYT1hWamVVSkVXbGhLTUdGWFduQlpNa1l3WVZjNWRVbEZSakZrUjJoMlkyMXNNR1ZVUlV4TlFXdEhRVEZWUlVOM2QwTlNlbGw0UlhwQlVrSm5UbFpDUVc5TlEydEdkMk5IZUd4SlJXeDFXWGswZUVONlFVcENaMDVXUWtGWlZFRnNWbFJOUWpSWVJGUkpNVTFFYTNoUFZFVTFUa1JSTVUxV2IxaEVWRWt6VFZSQmVFMTZSVE5PUkdONVRURnZkMmRhU1hoUlJFRXJRbWRPVmtKQlRVMU9NVUo1WWpKUloxSlZUa1JKUlRGb1dYbENRbU5JUVdkVk0xSjJZMjFWWjFsWE5XdEpSMnhWWkZjMWJHTjVRbFJrUnpsNVdsTkNVMXBYVG14aFdFSXdTVVpPY0ZveU5YQmliV040VEVSQmNVSm5UbFpDUVhOTlNUQkdkMk5IZUd4SlJtUjJZMjE0YTJReWJHdGFVMEpGV2xoYWJHSkhPWGRhV0VsblZXMVdjMWxZVW5CaU1qVjZUVkpOZDBWUldVUldVVkZMUkVGd1FtTklRbk5hVTBKS1ltMU5kVTFSYzNkRFVWbEVWbEZSUjBWM1NsWlZla0phVFVKTlIwSjVjVWRUVFRRNVFXZEZSME5EY1VkVFRUUTVRWGRGU0VFd1NVRkNUbTVXZG1oamRqZHBWQ3MzUlhnMWRFSk5RbWR5VVhOd1NIcEpjMWhTYVRCWmVHWmxhemRzZGpoM1JXMXFMMkpJYVZkMFRuZEtjV015UW05SWVuTlJhVVZxVURkTFJrbEpTMmMwV1RoNU1DOXVlVzUxUVcxcVoyZEpTVTFKU1VOQ1JFRk5RbWRPVmtoU1RVSkJaamhGUVdwQlFVMUNPRWRCTVZWa1NYZFJXVTFDWVVGR1JEaDJiRU5PVWpBeFJFcHRhV2M1TjJKQ09EVmpLMnhyUjB0YVRVaEJSME5EYzBkQlVWVkdRbmRGUWtKSFVYZFpha0YwUW1kbmNrSm5SVVpDVVdOM1FXOVphR0ZJVWpCalJHOTJUREpPYkdOdVVucE1iVVozWTBkNGJFeHRUblppVXprelpESlNlVnA2V1hWYVIxWjVUVVJGUjBORGMwZEJVVlZHUW5wQlFtaHBWbTlrU0ZKM1QyazRkbUl5VG5walF6Vm9ZMGhDYzFwVE5XcGlNakIyWWpKT2VtTkVRWHBNV0dReldraEtiazVxUVhsTlNVbENTR2RaUkZaU01HZENTVWxDUmxSRFEwRlNSWGRuWjBWT1FtZHZjV2hyYVVjNU1rNXJRbEZaUWsxSlNDdE5TVWhFUW1kbmNrSm5SVVpDVVdORFFXcERRblJuZVVKek1VcHNZa2RzYUdKdFRteEpSemwxU1VoU2IyRllUV2RaTWxaNVpFZHNiV0ZYVG1oa1IxVm5XVzVyWjFsWE5UVkpTRUpvWTI1U05VbEhSbnBqTTFaMFdsaE5aMWxYVG1wYVdFSXdXVmMxYWxwVFFuWmFhVUl3WVVkVloyUkhhR3hpYVVKb1kwaENjMkZYVG1oWmJYaHNTVWhPTUZsWE5XdFpXRXByU1VoU2JHTnRNWHBKUjBaMVdrTkNhbUl5Tld0aFdGSndZakkxZWtsSE9XMUpTRlo2V2xOM1oxa3lWbmxrUjJ4dFlWZE9hR1JIVldkalJ6bHpZVmRPTlVsSFJuVmFRMEpxV2xoS01HRlhXbkJaTWtZd1lWYzVkVWxJUW5sWlYwNHdZVmRPYkVsSVRqQlpXRkpzWWxkV2RXUklUWFZOUkZsSFEwTnpSMEZSVlVaQ2QwbENSbWx3YjJSSVVuZFBhVGgyWkROa00weHRSbmRqUjNoc1RHMU9kbUpUT1dwYVdFb3dZVmRhY0ZreVJqQmFWMFl4WkVkb2RtTnRiREJsVXpoM1NGRlpSRlpTTUU5Q1FsbEZSa2xHYVc5SE5IZE5UVlpCTVd0MU9YcEtiVWRPVUVGV2JqTmxjVTFCTkVkQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlVVSm5iM0ZvYTJsSE9USk9hMEpuYzBKQ1FVbEdRVVJCUzBKblozRm9hMnBQVUZGUlJFRjNUbkJCUkVKdFFXcEZRU3R4V0c1U1JVTTNhRmhKVjFaTWMweDRlbTVxVW5CSmVsQm1OMVpJZWpsV0wwTlViVGdyVEVwc2NsRmxjRzV0WTFCMlIweE9ZMWcyV0ZCdWJHTm5URUZCYWtWQk5VbHFUbHBMWjJjMWNGRTNPV3R1UmpSSllsUllaRXQyT0haMWRFbEVUVmhFYldwUVZsUXpaRWQyUm5SelIxSjNXRTk1ZDFJeWExcERaRk55Wm1WdmRDSXNJazFKU1VSR2FrTkRRWEI1WjBGM1NVSkJaMGxWU1hOSGFGSjNjREJqTW01MlZUUlpVM2xqWVdaUVZHcDZZazVqZDBObldVbExiMXBKZW1vd1JVRjNUWGRhZWtWaVRVSnJSMEV4VlVWQmQzZFRVVmhDZDJKSFZXZFZiVGwyWkVOQ1JGRlRRWFJKUldONlRWTlpkMHBCV1VSV1VWRk1SRUl4UW1OSVFuTmFVMEpFV2xoS01HRlhXbkJaTWtZd1lWYzVkVWxGUmpGa1IyaDJZMjFzTUdWVVJWUk5Ra1ZIUVRGVlJVTm5kMHRSV0VKM1lrZFZaMU5YTldwTWFrVk1UVUZyUjBFeFZVVkNhRTFEVmxaTmQwaG9ZMDVOYWtWM1RYcEZNMDFxUVhwT2VrVjNWMmhqVGsxNldYZE5la1UxVFVSQmQwMUVRWGRYYWtJeFRWVlJkMUZuV1VSV1VWRkVSRVIwUW1OSVFuTmFVMEpZWWpOS2MxcElaSEJhUjFWblVrZFdNbHBYZUhaalIxWjVTVVpLYkdKSFJqQmhWemwxWTNsQ1JGcFlTakJoVjFwd1dUSkdNR0ZYT1hWSlJVWXhaRWRvZG1OdGJEQmxWRVZNVFVGclIwRXhWVVZEZDNkRFVucFplRVY2UVZKQ1owNVdRa0Z2VFVOclJuZGpSM2hzU1VWc2RWbDVOSGhEZWtGS1FtZE9Wa0pCV1ZSQmJGWlVUVWhaZDBWQldVaExiMXBKZW1vd1EwRlJXVVpMTkVWRlFVTkpSRmxuUVVWaWMxRkxRemswVUhKc1YyMWFXRzVZWjNSNGVtUldTa3c0VkRCVFIxbHVaMFJTUjNCdVoyNHpUalpRVkRoS1RVVmlOMFpFYVRSaVFtMVFhRU51V2pNdmMzRTJVRVl2WTBkalMxaFhjMHcxZGs5MFpWSm9lVW8wTlhnelFWTlFOMk5QUWl0aFlXODVNR1pqY0hoVGRpOUZXa1ppYm1sQllrNW5Xa2RvU1dod1NXODBTRFpOU1VnelRVSkpSMEV4VldSRmQwVkNMM2RSU1UxQldVSkJaamhEUVZGQmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZWMU4wUmxiMVpuZW1sS2NXdHBjRzVsZG5JemNuSTVja3hLUzNOM1VtZFpTVXQzV1VKQ1VWVklRVkZGUlU5cVFUUk5SRmxIUTBOelIwRlJWVVpDZWtGQ2FHbHdiMlJJVW5kUGFUaDJZakpPZW1ORE5XaGpTRUp6V2xNMWFtSXlNSFppTWs1NlkwUkJla3hYUm5kalIzaHNZMjA1ZG1SSFRtaGFlazEzVG5kWlJGWlNNR1pDUkVGM1RHcEJjMjlEY1dkTFNWbHRZVWhTTUdORWIzWk1NazU1WWtNMWFHTklRbk5hVXpWcVlqSXdkbGxZUW5kaVIxWjVZakk1TUZreVJtNU5lVFZxWTIxM2QwaFJXVVJXVWpCUFFrSlpSVVpFT0hac1EwNVNNREZFU20xcFp6azNZa0k0TldNcmJHdEhTMXBOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKUWtKcVFWRkNaMjl4YUd0cFJ6a3lUbXRDWjBsQ1FrRkpSa0ZFUVV0Q1oyZHhhR3RxVDFCUlVVUkJkMDV2UVVSQ2JFRnFRa0ZZYUZOeE5VbDVTMjluVFVOUWRIYzBPVEJDWVVJMk56ZERZVVZIU2xoMVpsRkNMMFZ4V2tka05rTlRhbWxEZEU5dWRVMVVZbGhXV0cxNGVHTjRabXREVFZGRVZGTlFlR0Z5V2xoMlRuSnJlRlV6Vkd0VlRVa3pNM2w2ZGtaV1ZsSlVOSGQ0VjBwRE9UazBUM05rWTFvMEsxSkhUbk5aUkhsU05XZHRaSEl3YmtSSFp6MGlMQ0pOU1VsRFVYcERRMEZqYldkQmQwbENRV2RKU1V4aldEaHBUa3hHVXpWVmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhkYWVrVmlUVUpyUjBFeFZVVkJkM2RUVVZoQ2QySkhWV2RWYlRsMlpFTkNSRkZUUVhSSlJXTjZUVk5aZDBwQldVUldVVkZNUkVJeFFtTklRbk5hVTBKRVdsaEtNR0ZYV25CWk1rWXdZVmM1ZFVsRlJqRmtSMmgyWTIxc01HVlVSVlJOUWtWSFFURlZSVU5uZDB0UldFSjNZa2RWWjFOWE5XcE1ha1ZNVFVGclIwRXhWVVZDYUUxRFZsWk5kMGhvWTA1TlZGRjNUa1JOZDAxVVozaFBWRUV5VjJoalRrMTZhM2RPUkUxM1RWUm5lRTlVUVRKWGFrSnVUVkp6ZDBkUldVUldVVkZFUkVKS1FtTklRbk5hVTBKVFlqSTVNRWxGVGtKSlF6Qm5VbnBOZUVwcVFXdENaMDVXUWtGelRVaFZSbmRqUjNoc1NVVk9iR051VW5CYWJXeHFXVmhTY0dJeU5HZFJXRll3WVVjNWVXRllValZOVWsxM1JWRlpSRlpSVVV0RVFYQkNZMGhDYzFwVFFrcGliVTExVFZGemQwTlJXVVJXVVZGSFJYZEtWbFY2UWpKTlFrRkhRbmx4UjFOTk5EbEJaMFZIUWxOMVFrSkJRV2xCTWtsQlFrcHFjRXg2TVVGamNWUjBhM2xLZVdkU1RXTXpVa05XT0dOWGFsUnVTR05HUW1KYVJIVlhiVUpUY0ROYVNIUm1WR3BxVkhWNGVFVjBXQzh4U0RkWmVWbHNNMG8yV1ZKaVZIcENVRVZXYjBFdlZtaFpSRXRZTVVSNWVFNUNNR05VWkdSeFdHdzFaSFpOVm5wMFN6VXhOMGxFZGxsMVZsUmFXSEJ0YTA5c1JVdE5ZVTVEVFVWQmQwaFJXVVJXVWpCUFFrSlpSVVpNZFhjemNVWlpUVFJwWVhCSmNWb3pjalk1TmpZdllYbDVVM0pOUVRoSFFURlZaRVYzUlVJdmQxRkdUVUZOUWtGbU9IZEVaMWxFVmxJd1VFRlJTQzlDUVZGRVFXZEZSMDFCYjBkRFEzRkhVMDAwT1VKQlRVUkJNbWRCVFVkVlEwMVJRMFEyWTBoRlJtdzBZVmhVVVZreVpUTjJPVWQzVDBGRldreDFUaXQ1VW1oSVJrUXZNMjFsYjNsb2NHMTJUM2RuVUZWdVVGZFVlRzVUTkdGMEszRkplRlZEVFVjeGJXbG9SRXN4UVROVlZEZ3lUbEY2TmpCcGJVOXNUVEkzYW1Ka2IxaDBNbEZtZVVaTmJTdFphR2xrUkd0TVJqRjJURlZoWjAwMlFtZEVOVFpMZVV0QlBUMGlYWDAuZXlKMGNtRnVjMkZqZEdsdmJrbGtJam9pTWpBd01EQXdNVEExTURVMU1qRXpPU0lzSW05eWFXZHBibUZzVkhKaGJuTmhZM1JwYjI1SlpDSTZJakl3TURBd01ERXdOVEExTWpZNE9UY2lMQ0ozWldKUGNtUmxja3hwYm1WSmRHVnRTV1FpT2lJeU1EQXdNREF3TVRFM05UYzVNRFUySWl3aVluVnVaR3hsU1dRaU9pSmpiMjB1YldsdVozbDFaUzVzYVc1bmVHa2lMQ0p3Y205a2RXTjBTV1FpT2lKamIyMHViV2x1WjNsMVpTNXdjbTlrZFdOMExtMXZiblJvSWl3aWMzVmljMk55YVhCMGFXOXVSM0p2ZFhCSlpHVnVkR2xtYVdWeUlqb2lNakU0TVRrMU16TWlMQ0p3ZFhKamFHRnpaVVJoZEdVaU9qRTNOakkwTXprd05EUXdNREFzSW05eWFXZHBibUZzVUhWeVkyaGhjMlZFWVhSbElqb3hOell5TkRNMk9UUTBNREF3TENKbGVIQnBjbVZ6UkdGMFpTSTZNVGMyTWpRek9UTTBOREF3TUN3aWNYVmhiblJwZEhraU9qRXNJblI1Y0dVaU9pSkJkWFJ2TFZKbGJtVjNZV0pzWlNCVGRXSnpZM0pwY0hScGIyNGlMQ0pwYmtGd2NFOTNibVZ5YzJocGNGUjVjR1VpT2lKUVZWSkRTRUZUUlVRaUxDSnphV2R1WldSRVlYUmxJam94TnpZeU5ETTVNREE0TnpjeUxDSmxiblpwY205dWJXVnVkQ0k2SWxOaGJtUmliM2dpTENKMGNtRnVjMkZqZEdsdmJsSmxZWE52YmlJNklsSkZUa1ZYUVV3aUxDSnpkRzl5WldaeWIyNTBJam9pUTBoT0lpd2ljM1J2Y21WbWNtOXVkRWxrSWpvaU1UUXpORFkxSWl3aWNISnBZMlVpT2pFNE1EQXdMQ0pqZFhKeVpXNWplU0k2SWtOT1dTSXNJbUZ3Y0ZSeVlXNXpZV04wYVc5dVNXUWlPaUkzTURVd01EQTBPVEUxTmpBM09URTRPVElpZlEub213eEJVTHlweWxsaURMRmJaQnlSR1RQZW80YVMzZERUVFAwUmdyV2tITldaZGtWcUpDRXlCNEZlbnpUWDllVV9NNm5SNmxoUHltUndrTVpPM0kyZnciLCJzaWduZWRSZW5ld2FsSW5mbyI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5nMVl5STZXeUpOU1VsRlRWUkRRMEUzWVdkQmQwbENRV2RKVVZJNFMwaDZaRzQxTlRSYUwxVnZjbUZrVG5nNWRIcEJTMEpuWjNGb2EycFBVRkZSUkVGNlFqRk5WVkYzVVdkWlJGWlJVVVJFUkhSQ1kwaENjMXBUUWxoaU0wcHpXa2hrY0ZwSFZXZFNSMVl5V2xkNGRtTkhWbmxKUmtwc1lrZEdNR0ZYT1hWamVVSkVXbGhLTUdGWFduQlpNa1l3WVZjNWRVbEZSakZrUjJoMlkyMXNNR1ZVUlV4TlFXdEhRVEZWUlVOM2QwTlNlbGw0UlhwQlVrSm5UbFpDUVc5TlEydEdkMk5IZUd4SlJXeDFXWGswZUVONlFVcENaMDVXUWtGWlZFRnNWbFJOUWpSWVJGUkpNVTFFYTNoUFZFVTFUa1JSTVUxV2IxaEVWRWt6VFZSQmVFMTZSVE5PUkdONVRURnZkMmRhU1hoUlJFRXJRbWRPVmtKQlRVMU9NVUo1WWpKUloxSlZUa1JKUlRGb1dYbENRbU5JUVdkVk0xSjJZMjFWWjFsWE5XdEpSMnhWWkZjMWJHTjVRbFJrUnpsNVdsTkNVMXBYVG14aFdFSXdTVVpPY0ZveU5YQmliV040VEVSQmNVSm5UbFpDUVhOTlNUQkdkMk5IZUd4SlJtUjJZMjE0YTJReWJHdGFVMEpGV2xoYWJHSkhPWGRhV0VsblZXMVdjMWxZVW5CaU1qVjZUVkpOZDBWUldVUldVVkZMUkVGd1FtTklRbk5hVTBKS1ltMU5kVTFSYzNkRFVWbEVWbEZSUjBWM1NsWlZla0phVFVKTlIwSjVjVWRUVFRRNVFXZEZSME5EY1VkVFRUUTVRWGRGU0VFd1NVRkNUbTVXZG1oamRqZHBWQ3MzUlhnMWRFSk5RbWR5VVhOd1NIcEpjMWhTYVRCWmVHWmxhemRzZGpoM1JXMXFMMkpJYVZkMFRuZEtjV015UW05SWVuTlJhVVZxVURkTFJrbEpTMmMwV1RoNU1DOXVlVzUxUVcxcVoyZEpTVTFKU1VOQ1JFRk5RbWRPVmtoU1RVSkJaamhGUVdwQlFVMUNPRWRCTVZWa1NYZFJXVTFDWVVGR1JEaDJiRU5PVWpBeFJFcHRhV2M1TjJKQ09EVmpLMnhyUjB0YVRVaEJSME5EYzBkQlVWVkdRbmRGUWtKSFVYZFpha0YwUW1kbmNrSm5SVVpDVVdOM1FXOVphR0ZJVWpCalJHOTJUREpPYkdOdVVucE1iVVozWTBkNGJFeHRUblppVXprelpESlNlVnA2V1hWYVIxWjVUVVJGUjBORGMwZEJVVlZHUW5wQlFtaHBWbTlrU0ZKM1QyazRkbUl5VG5walF6Vm9ZMGhDYzFwVE5XcGlNakIyWWpKT2VtTkVRWHBNV0dReldraEtiazVxUVhsTlNVbENTR2RaUkZaU01HZENTVWxDUmxSRFEwRlNSWGRuWjBWT1FtZHZjV2hyYVVjNU1rNXJRbEZaUWsxSlNDdE5TVWhFUW1kbmNrSm5SVVpDVVdORFFXcERRblJuZVVKek1VcHNZa2RzYUdKdFRteEpSemwxU1VoU2IyRllUV2RaTWxaNVpFZHNiV0ZYVG1oa1IxVm5XVzVyWjFsWE5UVkpTRUpvWTI1U05VbEhSbnBqTTFaMFdsaE5aMWxYVG1wYVdFSXdXVmMxYWxwVFFuWmFhVUl3WVVkVloyUkhhR3hpYVVKb1kwaENjMkZYVG1oWmJYaHNTVWhPTUZsWE5XdFpXRXByU1VoU2JHTnRNWHBKUjBaMVdrTkNhbUl5Tld0aFdGSndZakkxZWtsSE9XMUpTRlo2V2xOM1oxa3lWbmxrUjJ4dFlWZE9hR1JIVldkalJ6bHpZVmRPTlVsSFJuVmFRMEpxV2xoS01HRlhXbkJaTWtZd1lWYzVkVWxJUW5sWlYwNHdZVmRPYkVsSVRqQlpXRkpzWWxkV2RXUklUWFZOUkZsSFEwTnpSMEZSVlVaQ2QwbENSbWx3YjJSSVVuZFBhVGgyWkROa00weHRSbmRqUjNoc1RHMU9kbUpUT1dwYVdFb3dZVmRhY0ZreVJqQmFWMFl4WkVkb2RtTnRiREJsVXpoM1NGRlpSRlpTTUU5Q1FsbEZSa2xHYVc5SE5IZE5UVlpCTVd0MU9YcEtiVWRPVUVGV2JqTmxjVTFCTkVkQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlVVSm5iM0ZvYTJsSE9USk9hMEpuYzBKQ1FVbEdRVVJCUzBKblozRm9hMnBQVUZGUlJFRjNUbkJCUkVKdFFXcEZRU3R4V0c1U1JVTTNhRmhKVjFaTWMweDRlbTVxVW5CSmVsQm1OMVpJZWpsV0wwTlViVGdyVEVwc2NsRmxjRzV0WTFCMlIweE9ZMWcyV0ZCdWJHTm5URUZCYWtWQk5VbHFUbHBMWjJjMWNGRTNPV3R1UmpSSllsUllaRXQyT0haMWRFbEVUVmhFYldwUVZsUXpaRWQyUm5SelIxSjNXRTk1ZDFJeWExcERaRk55Wm1WdmRDSXNJazFKU1VSR2FrTkRRWEI1WjBGM1NVSkJaMGxWU1hOSGFGSjNjREJqTW01MlZUUlpVM2xqWVdaUVZHcDZZazVqZDBObldVbExiMXBKZW1vd1JVRjNUWGRhZWtWaVRVSnJSMEV4VlVWQmQzZFRVVmhDZDJKSFZXZFZiVGwyWkVOQ1JGRlRRWFJKUldONlRWTlpkMHBCV1VSV1VWRk1SRUl4UW1OSVFuTmFVMEpFV2xoS01HRlhXbkJaTWtZd1lWYzVkVWxGUmpGa1IyaDJZMjFzTUdWVVJWUk5Ra1ZIUVRGVlJVTm5kMHRSV0VKM1lrZFZaMU5YTldwTWFrVk1UVUZyUjBFeFZVVkNhRTFEVmxaTmQwaG9ZMDVOYWtWM1RYcEZNMDFxUVhwT2VrVjNWMmhqVGsxNldYZE5la1UxVFVSQmQwMUVRWGRYYWtJeFRWVlJkMUZuV1VSV1VWRkVSRVIwUW1OSVFuTmFVMEpZWWpOS2MxcElaSEJhUjFWblVrZFdNbHBYZUhaalIxWjVTVVpLYkdKSFJqQmhWemwxWTNsQ1JGcFlTakJoVjFwd1dUSkdNR0ZYT1hWSlJVWXhaRWRvZG1OdGJEQmxWRVZNVFVGclIwRXhWVVZEZDNkRFVucFplRVY2UVZKQ1owNVdRa0Z2VFVOclJuZGpSM2hzU1VWc2RWbDVOSGhEZWtGS1FtZE9Wa0pCV1ZSQmJGWlVUVWhaZDBWQldVaExiMXBKZW1vd1EwRlJXVVpMTkVWRlFVTkpSRmxuUVVWaWMxRkxRemswVUhKc1YyMWFXRzVZWjNSNGVtUldTa3c0VkRCVFIxbHVaMFJTUjNCdVoyNHpUalpRVkRoS1RVVmlOMFpFYVRSaVFtMVFhRU51V2pNdmMzRTJVRVl2WTBkalMxaFhjMHcxZGs5MFpWSm9lVW8wTlhnelFWTlFOMk5QUWl0aFlXODVNR1pqY0hoVGRpOUZXa1ppYm1sQllrNW5Xa2RvU1dod1NXODBTRFpOU1VnelRVSkpSMEV4VldSRmQwVkNMM2RSU1UxQldVSkJaamhEUVZGQmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZWMU4wUmxiMVpuZW1sS2NXdHBjRzVsZG5JemNuSTVja3hLUzNOM1VtZFpTVXQzV1VKQ1VWVklRVkZGUlU5cVFUUk5SRmxIUTBOelIwRlJWVVpDZWtGQ2FHbHdiMlJJVW5kUGFUaDJZakpPZW1ORE5XaGpTRUp6V2xNMWFtSXlNSFppTWs1NlkwUkJla3hYUm5kalIzaHNZMjA1ZG1SSFRtaGFlazEzVG5kWlJGWlNNR1pDUkVGM1RHcEJjMjlEY1dkTFNWbHRZVWhTTUdORWIzWk1NazU1WWtNMWFHTklRbk5hVXpWcVlqSXdkbGxZUW5kaVIxWjVZakk1TUZreVJtNU5lVFZxWTIxM2QwaFJXVVJXVWpCUFFrSlpSVVpFT0hac1EwNVNNREZFU20xcFp6azNZa0k0TldNcmJHdEhTMXBOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKUWtKcVFWRkNaMjl4YUd0cFJ6a3lUbXRDWjBsQ1FrRkpSa0ZFUVV0Q1oyZHhhR3RxVDFCUlVVUkJkMDV2UVVSQ2JFRnFRa0ZZYUZOeE5VbDVTMjluVFVOUWRIYzBPVEJDWVVJMk56ZERZVVZIU2xoMVpsRkNMMFZ4V2tka05rTlRhbWxEZEU5dWRVMVVZbGhXV0cxNGVHTjRabXREVFZGRVZGTlFlR0Z5V2xoMlRuSnJlRlV6Vkd0VlRVa3pNM2w2ZGtaV1ZsSlVOSGQ0VjBwRE9UazBUM05rWTFvMEsxSkhUbk5aUkhsU05XZHRaSEl3YmtSSFp6MGlMQ0pOU1VsRFVYcERRMEZqYldkQmQwbENRV2RKU1V4aldEaHBUa3hHVXpWVmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhkYWVrVmlUVUpyUjBFeFZVVkJkM2RUVVZoQ2QySkhWV2RWYlRsMlpFTkNSRkZUUVhSSlJXTjZUVk5aZDBwQldVUldVVkZNUkVJeFFtTklRbk5hVTBKRVdsaEtNR0ZYV25CWk1rWXdZVmM1ZFVsRlJqRmtSMmgyWTIxc01HVlVSVlJOUWtWSFFURlZSVU5uZDB0UldFSjNZa2RWWjFOWE5XcE1ha1ZNVFVGclIwRXhWVVZDYUUxRFZsWk5kMGhvWTA1TlZGRjNUa1JOZDAxVVozaFBWRUV5VjJoalRrMTZhM2RPUkUxM1RWUm5lRTlVUVRKWGFrSnVUVkp6ZDBkUldVUldVVkZFUkVKS1FtTklRbk5hVTBKVFlqSTVNRWxGVGtKSlF6Qm5VbnBOZUVwcVFXdENaMDVXUWtGelRVaFZSbmRqUjNoc1NVVk9iR051VW5CYWJXeHFXVmhTY0dJeU5HZFJXRll3WVVjNWVXRllValZOVWsxM1JWRlpSRlpSVVV0RVFYQkNZMGhDYzFwVFFrcGliVTExVFZGemQwTlJXVVJXVVZGSFJYZEtWbFY2UWpKTlFrRkhRbmx4UjFOTk5EbEJaMFZIUWxOMVFrSkJRV2xCTWtsQlFrcHFjRXg2TVVGamNWUjBhM2xLZVdkU1RXTXpVa05XT0dOWGFsUnVTR05HUW1KYVJIVlhiVUpUY0ROYVNIUm1WR3BxVkhWNGVFVjBXQzh4U0RkWmVWbHNNMG8yV1ZKaVZIcENVRVZXYjBFdlZtaFpSRXRZTVVSNWVFNUNNR05VWkdSeFdHdzFaSFpOVm5wMFN6VXhOMGxFZGxsMVZsUmFXSEJ0YTA5c1JVdE5ZVTVEVFVWQmQwaFJXVVJXVWpCUFFrSlpSVVpNZFhjemNVWlpUVFJwWVhCSmNWb3pjalk1TmpZdllYbDVVM0pOUVRoSFFURlZaRVYzUlVJdmQxRkdUVUZOUWtGbU9IZEVaMWxFVmxJd1VFRlJTQzlDUVZGRVFXZEZSMDFCYjBkRFEzRkhVMDAwT1VKQlRVUkJNbWRCVFVkVlEwMVJRMFEyWTBoRlJtdzBZVmhVVVZreVpUTjJPVWQzVDBGRldreDFUaXQ1VW1oSVJrUXZNMjFsYjNsb2NHMTJUM2RuVUZWdVVGZFVlRzVUTkdGMEszRkplRlZEVFVjeGJXbG9SRXN4UVROVlZEZ3lUbEY2TmpCcGJVOXNUVEkzYW1Ka2IxaDBNbEZtZVVaTmJTdFphR2xrUkd0TVJqRjJURlZoWjAwMlFtZEVOVFpMZVV0QlBUMGlYWDAuZXlKdmNtbG5hVzVoYkZSeVlXNXpZV04wYVc5dVNXUWlPaUl5TURBd01EQXhNRFV3TlRJMk9EazNJaXdpWVhWMGIxSmxibVYzVUhKdlpIVmpkRWxrSWpvaVkyOXRMbTFwYm1kNWRXVXVjSEp2WkhWamRDNXRiMjUwYUNJc0luQnliMlIxWTNSSlpDSTZJbU52YlM1dGFXNW5lWFZsTG5CeWIyUjFZM1F1Ylc5dWRHZ2lMQ0poZFhSdlVtVnVaWGRUZEdGMGRYTWlPakVzSW5KbGJtVjNZV3hRY21salpTSTZNVGd3TURBc0ltTjFjbkpsYm1ONUlqb2lRMDVaSWl3aWMybG5ibVZrUkdGMFpTSTZNVGMyTWpRek9UQXdPRGMzTWl3aVpXNTJhWEp2Ym0xbGJuUWlPaUpUWVc1a1ltOTRJaXdpY21WalpXNTBVM1ZpYzJOeWFYQjBhVzl1VTNSaGNuUkVZWFJsSWpveE56WXlORE0yT1RRME1EQXdMQ0p5Wlc1bGQyRnNSR0YwWlNJNk1UYzJNalF6T1RNME5EQXdNQ3dpWVhCd1ZISmhibk5oWTNScGIyNUpaQ0k2SWpjd05UQXdNRFE1TVRVMk1EYzVNVGc1TWlKOS5Kby1hSm01LTF5ckpKREV4VWdneFRvc000RHQ5YVplUXhISU5JWmJXTjFIZk1TTDN6YUNaTUJpMW1CRGVibUJmdC14aUNLUTcwb3dEMHpucGVlSFpZdyIsInN0YXR1cyI6MX0sInZlcnNpb24iOiIyLjAiLCJzaWduZWREYXRlIjoxNzYyNDM5MDA4NzcyfQ.avFDmwK9Rj07moxVWu0l0SIBy-kEh0etVIRqMx4px6Wo4gRLIsVRQFigRa2B89q6y8orix-Dw-9zbTR4uRobRQ"); } + 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); + 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.decode(base64); + return new String(decodedBytes, StandardCharsets.UTF_8); + + } catch (Exception e) { + log.error("Base64URL解码失败: {}", e.getMessage()); + throw new RuntimeException("Base64解码失败", e); + } + } } + diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/util/InAppTransaction.java b/ruoyi-system/src/main/java/com/ruoyi/system/util/InAppTransaction.java new file mode 100644 index 0000000..963689c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/util/InAppTransaction.java @@ -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; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/util/NotificationInfo.java b/ruoyi-system/src/main/java/com/ruoyi/system/util/NotificationInfo.java new file mode 100644 index 0000000..d4ba39c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/util/NotificationInfo.java @@ -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; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/util/RenewalInfo.java b/ruoyi-system/src/main/java/com/ruoyi/system/util/RenewalInfo.java new file mode 100644 index 0000000..a21055c --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/util/RenewalInfo.java @@ -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; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/util/TransactionInfo.java b/ruoyi-system/src/main/java/com/ruoyi/system/util/TransactionInfo.java new file mode 100644 index 0000000..6d1b921 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/util/TransactionInfo.java @@ -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; +}