优化
This commit is contained in:
@@ -5,10 +5,13 @@ import cn.hutool.json.JSONUtil;
|
|||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.ruoyi.common.core.domain.entity.OrderInfo;
|
||||||
|
import com.ruoyi.common.utils.DateUtils;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import org.junit.Test;
|
import com.ruoyi.system.service.IOrderInfoService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
@@ -22,7 +25,8 @@ import java.net.URL;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.HashMap;
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +39,9 @@ public class AppleyPay {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(AppleyPay.class);
|
private static final Logger log = LoggerFactory.getLogger(AppleyPay.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IOrderInfoService orderInfoService;
|
||||||
|
|
||||||
//购买凭证验证地址
|
//购买凭证验证地址
|
||||||
private static final String certificateUrl = "https://buy.itunes.apple.com/verifyReceipt";
|
private static final String certificateUrl = "https://buy.itunes.apple.com/verifyReceipt";
|
||||||
|
|
||||||
@@ -86,17 +93,21 @@ public class AppleyPay {
|
|||||||
/**
|
/**
|
||||||
* 发送请求 向苹果发起验证支付请求是否有效:本方法有认证方法进行调用
|
* 发送请求 向苹果发起验证支付请求是否有效:本方法有认证方法进行调用
|
||||||
*
|
*
|
||||||
* com.mingyue.product.annual
|
* 支持的订阅产品ID:
|
||||||
* com.mingyue.product.semiAnnual
|
* - com.mingyue.product.month (月付)
|
||||||
* com.mingyue.product.quarterly
|
* - com.mingyue.product.quarterly (季付)
|
||||||
* com.mingyue.product.month
|
* - com.mingyue.product.semiAnnual (半年付)
|
||||||
* @param url 支付的环境校验
|
* - com.mingyue.product.annual (年付)
|
||||||
|
*
|
||||||
|
* @param url 支付的环境校验地址
|
||||||
* @param code 接口传递的 receipt
|
* @param code 接口传递的 receipt
|
||||||
|
* @param userId 用户ID
|
||||||
* @return 结果
|
* @return 结果
|
||||||
*/
|
*/
|
||||||
private String sendHttpsCoon(String url, String code, String userId) {
|
private String sendHttpsCoon(String url, String code, String userId) {
|
||||||
if (url.isEmpty()) {
|
if (url == null || url.isEmpty()) {
|
||||||
return null;
|
log.error("验证URL为空");
|
||||||
|
return "支付失败,验证URL为空";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
//设置SSLContext
|
//设置SSLContext
|
||||||
@@ -111,61 +122,259 @@ public class AppleyPay {
|
|||||||
conn.setRequestMethod("POST");
|
conn.setRequestMethod("POST");
|
||||||
conn.setDoOutput(true);
|
conn.setDoOutput(true);
|
||||||
conn.setRequestProperty("Content-type", "application/json");
|
conn.setRequestProperty("Content-type", "application/json");
|
||||||
|
conn.setConnectTimeout(10000); // 10秒连接超时
|
||||||
|
conn.setReadTimeout(10000); // 10秒读取超时
|
||||||
|
|
||||||
JSONObject obj = new JSONObject();
|
JSONObject obj = new JSONObject();
|
||||||
obj.put("receipt-data", code);
|
obj.put("receipt-data", code);
|
||||||
// 添加共享密钥 - 这里需要补充
|
// 添加共享密钥
|
||||||
obj.put("password", "96f11cd1f6714d1fb4206fcad92854bf");
|
obj.put("password", "96f11cd1f6714d1fb4206fcad92854bf");
|
||||||
|
|
||||||
BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream());
|
BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream());
|
||||||
buffOutStr.write(obj.toString().getBytes());
|
buffOutStr.write(obj.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
buffOutStr.flush();
|
buffOutStr.flush();
|
||||||
buffOutStr.close();
|
buffOutStr.close();
|
||||||
|
|
||||||
//获取输入流
|
//获取输入流
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
|
||||||
|
|
||||||
String line = null;
|
String line = null;
|
||||||
StringBuffer sb = new StringBuffer();
|
StringBuffer sb = new StringBuffer();
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
sb.append(line);
|
sb.append(line);
|
||||||
}
|
}
|
||||||
System.out.println(sb);
|
reader.close();
|
||||||
// 错误的 sb对象是:{"status":21002},苹果官网写的错误也都是2XXXX 具体含义可查:https://developer.apple.com/documentation/appstorereceipts/status
|
|
||||||
// 所以 通过长度等于16,我们就可确定苹果支付成功是否有效
|
|
||||||
if (sb.length() == 16) {
|
|
||||||
return "支付失败,苹果说status异常";
|
|
||||||
}
|
|
||||||
// 将一个Json对象转成一个HashMap
|
|
||||||
JSONObject alljsoncode = JSON.parseObject(sb.toString());
|
|
||||||
Object receipt = alljsoncode.get("latest_receipt_info");
|
|
||||||
// receipt 转成LinkList
|
|
||||||
List<InAppTransaction> inAppTransactions = JSONUtil.toList(receipt.toString(), InAppTransaction.class);
|
|
||||||
|
|
||||||
HashMap hashMap = JSONUtil.toBean(receipt.toString(), HashMap.class);
|
log.info("苹果验证响应: {}", sb.toString());
|
||||||
// 苹果给的订单号
|
|
||||||
String original_transaction_id = (String) hashMap.get("original_transaction_id");
|
// 解析响应
|
||||||
// 唯一ID
|
JSONObject alljsoncode = JSON.parseObject(sb.toString());
|
||||||
String transactionId = (String) hashMap.get("transaction_id");
|
Integer status = alljsoncode.getInteger("status");
|
||||||
//TODO 存储订单ID,并检查此订单ID是否存在,如果存在就证明已经发货了(避免二次发货)
|
|
||||||
if ("com.mingyue.product.month".equals(hashMap.get("product_id"))) {
|
// 检查状态码 (0表示成功,其他都是错误)
|
||||||
//TODO 月付
|
if (status == null || status != 0) {
|
||||||
log.info("用户ID:{},执行12元钻石追加",userId);
|
String errorMsg = getStatusErrorMessage(status);
|
||||||
} else if ("com.mingyue.product.quarterly".equals(hashMap.get("product_id"))) {
|
log.error("苹果验证失败,状态码: {}, 错误信息: {}", status, errorMsg);
|
||||||
//TODO 季付
|
return "支付失败,苹果验证失败: " + errorMsg;
|
||||||
log.info("用户ID:{},执行30元钻石追加",userId);
|
|
||||||
}else if ("com.mingyue.product.semiAnnual".equals(hashMap.get("product_id"))){
|
|
||||||
// TODo 半年付
|
|
||||||
}else if ("com.mingyue.product.annual".equals(hashMap.get("product_id"))){
|
|
||||||
// TODo 年付
|
|
||||||
} else {
|
|
||||||
log.info("用户ID:{},向苹果发起验证支付请求没有次product_id",userId);
|
|
||||||
return "支付失败,当前没有次product_id";
|
|
||||||
}
|
}
|
||||||
return "支付成功";
|
|
||||||
|
// 获取 latest_receipt_info,可能是数组或单个对象
|
||||||
|
Object latestReceiptInfoObj = alljsoncode.get("latest_receipt_info");
|
||||||
|
if (latestReceiptInfoObj == null) {
|
||||||
|
log.error("latest_receipt_info 为空");
|
||||||
|
return "支付失败,未找到交易信息";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 latest_receipt_info(可能是数组或单个对象)
|
||||||
|
InAppTransaction latestTransaction = null;
|
||||||
|
if (latestReceiptInfoObj instanceof JSONArray) {
|
||||||
|
// 如果是数组,取最后一个(最新的交易)
|
||||||
|
JSONArray receiptArray = (JSONArray) latestReceiptInfoObj;
|
||||||
|
if (receiptArray.isEmpty()) {
|
||||||
|
log.error("latest_receipt_info 数组为空");
|
||||||
|
return "支付失败,交易信息数组为空";
|
||||||
|
}
|
||||||
|
// 获取最新的交易记录(数组最后一个元素)
|
||||||
|
Object lastReceipt = receiptArray.get(receiptArray.size() - 1);
|
||||||
|
latestTransaction = JSONUtil.toBean(lastReceipt.toString(), InAppTransaction.class);
|
||||||
|
} else {
|
||||||
|
// 如果是单个对象
|
||||||
|
latestTransaction = JSONUtil.toBean(latestReceiptInfoObj.toString(), InAppTransaction.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestTransaction == null) {
|
||||||
|
log.error("无法解析交易信息");
|
||||||
|
return "支付失败,无法解析交易信息";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取关键信息
|
||||||
|
String transactionId = latestTransaction.getTransactionId();
|
||||||
|
String productId = latestTransaction.getProductId();
|
||||||
|
String expiresDateMs = latestTransaction.getExpiresDateMs();
|
||||||
|
|
||||||
|
if (StringUtils.isEmpty(transactionId)) {
|
||||||
|
log.error("transaction_id 为空");
|
||||||
|
return "支付失败,交易ID为空";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单是否已处理(防重复发货)
|
||||||
|
OrderInfo queryOrder = new OrderInfo();
|
||||||
|
queryOrder.setTradeNo(transactionId);
|
||||||
|
queryOrder.setPayType("applePay");
|
||||||
|
List<OrderInfo> existingOrders = orderInfoService.selectOrderInfoList(queryOrder);
|
||||||
|
if (existingOrders != null && !existingOrders.isEmpty()) {
|
||||||
|
log.warn("交易ID {} 已存在,可能重复处理。用户ID: {}", transactionId, userId);
|
||||||
|
// 如果订单已存在且已支付,返回成功(幂等性处理)
|
||||||
|
OrderInfo existingOrder = existingOrders.get(0);
|
||||||
|
if (existingOrder.getPayStatus() != null && existingOrder.getPayStatus() >= 2) {
|
||||||
|
log.info("订单已处理,返回成功。订单ID: {}, 交易ID: {}", existingOrder.getOrderId(), transactionId);
|
||||||
|
return "支付成功";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证订阅是否过期(对于订阅类型)
|
||||||
|
if (StringUtils.isNotEmpty(expiresDateMs)) {
|
||||||
|
try {
|
||||||
|
long expiresTimestamp = Long.parseLong(expiresDateMs);
|
||||||
|
long currentTimestamp = System.currentTimeMillis();
|
||||||
|
if (expiresTimestamp < currentTimestamp) {
|
||||||
|
log.warn("订阅已过期。交易ID: {}, 过期时间: {}, 当前时间: {}",
|
||||||
|
transactionId, new Date(expiresTimestamp), new Date(currentTimestamp));
|
||||||
|
return "支付失败,订阅已过期";
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("无法解析过期时间: {}", expiresDateMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据产品ID处理不同的订阅类型
|
||||||
|
String packageType = null;
|
||||||
|
String orderName = null;
|
||||||
|
Long amount = null; // 金额(分)
|
||||||
|
|
||||||
|
if ("com.mingyue.product.month".equals(productId)) {
|
||||||
|
// 月付
|
||||||
|
packageType = "1";
|
||||||
|
orderName = "VIP会员包月";
|
||||||
|
amount = 1200L; // 12元 = 1200分
|
||||||
|
log.info("用户ID:{}, 产品:月付订阅", userId);
|
||||||
|
} else if ("com.mingyue.product.quarterly".equals(productId)) {
|
||||||
|
// 季付
|
||||||
|
packageType = "2";
|
||||||
|
orderName = "VIP会员包季度";
|
||||||
|
amount = 3000L; // 30元 = 3000分
|
||||||
|
log.info("用户ID:{}, 产品:季付订阅", userId);
|
||||||
|
} else if ("com.mingyue.product.semiAnnual".equals(productId)) {
|
||||||
|
// 半年付
|
||||||
|
packageType = "3";
|
||||||
|
orderName = "VIP会员包半年";
|
||||||
|
amount = 6000L; // 60元 = 6000分(假设)
|
||||||
|
log.info("用户ID:{}, 产品:半年付订阅", userId);
|
||||||
|
} else if ("com.mingyue.product.annual".equals(productId)) {
|
||||||
|
// 年付
|
||||||
|
packageType = "4";
|
||||||
|
orderName = "VIP会员包年";
|
||||||
|
amount = 12000L; // 120元 = 12000分(假设)
|
||||||
|
log.info("用户ID:{}, 产品:年付订阅", userId);
|
||||||
|
} else {
|
||||||
|
log.error("用户ID:{}, 未知的产品ID: {}", userId, productId);
|
||||||
|
return "支付失败,未知的产品ID: " + productId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建或更新订单
|
||||||
|
try {
|
||||||
|
// 计算服务开始和结束时间
|
||||||
|
Date startTime = new Date();
|
||||||
|
Date endTime = calculateEndTime(startTime, packageType);
|
||||||
|
|
||||||
|
// 创建订单记录
|
||||||
|
OrderInfo orderInfo = new OrderInfo();
|
||||||
|
orderInfo.setId(java.util.UUID.randomUUID().toString().replace("-", ""));
|
||||||
|
orderInfo.setOrderId(System.currentTimeMillis());
|
||||||
|
orderInfo.setOrderName(orderName);
|
||||||
|
orderInfo.setUserId(Long.parseLong(userId));
|
||||||
|
orderInfo.setAmount(amount);
|
||||||
|
orderInfo.setPayType("applePay");
|
||||||
|
orderInfo.setPayStatus(2L); // 2-待出货
|
||||||
|
orderInfo.setPayTime(DateUtils.getNowDate());
|
||||||
|
orderInfo.setStartTime(startTime);
|
||||||
|
orderInfo.setEndTime(endTime);
|
||||||
|
orderInfo.setPackageType(packageType);
|
||||||
|
orderInfo.setTradeNo(transactionId);
|
||||||
|
orderInfo.setCallbackContent(sb.toString()); // 保存完整的回调内容
|
||||||
|
orderInfo.setCallTime(DateUtils.getNowDate());
|
||||||
|
orderInfo.setIdDel(0L);
|
||||||
|
orderInfo.setVersion(1L);
|
||||||
|
orderInfo.setCreateTime(DateUtils.getNowDate());
|
||||||
|
orderInfo.setUpdateTime(DateUtils.getNowDate());
|
||||||
|
|
||||||
|
// 保存订单
|
||||||
|
int result = orderInfoService.insertOrderInfo(orderInfo);
|
||||||
|
if (result > 0) {
|
||||||
|
log.info("订单创建成功。订单ID: {}, 交易ID: {}, 用户ID: {}, 产品: {}",
|
||||||
|
orderInfo.getOrderId(), transactionId, userId, productId);
|
||||||
|
return "支付成功";
|
||||||
|
} else {
|
||||||
|
log.error("订单创建失败。交易ID: {}, 用户ID: {}", transactionId, userId);
|
||||||
|
return "支付失败,订单创建失败";
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建订单异常。交易ID: {}, 用户ID: {}, 异常: {}", transactionId, userId, e.getMessage(), e);
|
||||||
|
return "支付失败,订单创建异常: " + e.getMessage();
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("向苹果发起验证支付请求是否有效出现异常:{}", e.getMessage());
|
log.error("向苹果发起验证支付请求是否有效出现异常:{}", e.getMessage(), e);
|
||||||
return "支付过程中,出现了异常!";
|
return "支付过程中,出现了异常: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据套餐类型计算结束时间
|
||||||
|
*
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param packageType 套餐类型:1-月付, 3-季付, 6-半年付, 12-年付
|
||||||
|
* @return 结束时间
|
||||||
|
*/
|
||||||
|
private Date calculateEndTime(Date startTime, String packageType) {
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.setTime(startTime);
|
||||||
|
|
||||||
|
switch (packageType) {
|
||||||
|
case "1": // 月付
|
||||||
|
calendar.add(Calendar.MONTH, 1);
|
||||||
|
break;
|
||||||
|
case "3": // 季付
|
||||||
|
calendar.add(Calendar.MONTH, 3);
|
||||||
|
break;
|
||||||
|
case "6": // 半年付
|
||||||
|
calendar.add(Calendar.MONTH, 6);
|
||||||
|
break;
|
||||||
|
case "12": // 年付
|
||||||
|
calendar.add(Calendar.YEAR, 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
calendar.add(Calendar.MONTH, 1); // 默认1个月
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return calendar.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取苹果状态码对应的错误信息
|
||||||
|
*
|
||||||
|
* @param status 状态码
|
||||||
|
* @return 错误信息
|
||||||
|
*/
|
||||||
|
private String getStatusErrorMessage(Integer status) {
|
||||||
|
if (status == null) {
|
||||||
|
return "未知错误";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 0:
|
||||||
|
return "成功";
|
||||||
|
case 21000:
|
||||||
|
return "App Store无法读取你提供的JSON数据";
|
||||||
|
case 21002:
|
||||||
|
return "receipt-data属性中的数据格式错误或丢失";
|
||||||
|
case 21003:
|
||||||
|
return "收据无法验证";
|
||||||
|
case 21004:
|
||||||
|
return "你提供的共享密钥与账户中的共享密钥不匹配";
|
||||||
|
case 21005:
|
||||||
|
return "收据服务器当前不可用";
|
||||||
|
case 21006:
|
||||||
|
return "此收据有效,但订阅已过期";
|
||||||
|
case 21007:
|
||||||
|
return "此收据来自测试环境,但发送到生产环境进行验证";
|
||||||
|
case 21008:
|
||||||
|
return "此收据来自生产环境,但发送到测试环境进行验证";
|
||||||
|
case 21010:
|
||||||
|
return "此收据无法被授权";
|
||||||
|
default:
|
||||||
|
return "未知错误,状态码: " + status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user