0%

springboot接入苹果内购

苹果内购

在上一篇给大家描述了google支付的集成后,苹果内购也是必不可少的。就我而言,感觉苹果内购比google支付要简单容易得多,因为苹果内购撸代码前不需要配置准备,只需要在代码里集成就行了。废话少说,接下来还是分享一下我是如何集成苹果内购的。
首先需要一个苹果支付工具类,这里是IosVerifyUtil.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package com.xy.goone.common.util.pay;

import javax.net.ssl.*;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Locale;

/**
* @author fumei.jiang
* @date 2019-08-15 18:31
* @Description 苹果内购工具类
*/
public class IosVerifyUtil {
private static class TrustAnyTrustManager implements X509TrustManager {

public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}

public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}

public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[] {};
}
}

private static class TrustAnyHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session) {
return true;
}
}

private static final String url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt";//沙盒测试
private static final String url_verify = "https://buy.itunes.apple.com/verifyReceipt";//正式测试
private static final String IOS_SHARED_SECRET_PASSWORD = "64dd649b6e4d4d6d9a6b99a28dc26320";//苹果连续订阅共享密钥

/**
* 苹果服务器验证
*
* @param receipt
* 账单
* @url 要验证的地址
* @return null 或返回结果 沙盒 https://sandbox.itunes.apple.com/verifyReceipt
*
*/
public static String buyAppVerify(String receipt,int type,int status) {
//环境判断 线上/开发环境用不同的请求链接
String url = "";
if(type==0){
url = url_sandbox; //沙盒测试
}else{
url = url_verify; //线上测试
}
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
URL console = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
conn.setSSLSocketFactory(sc.getSocketFactory());
conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
conn.setRequestMethod("POST");
conn.setRequestProperty("content-type", "text/json");
conn.setRequestProperty("Proxy-Connection", "Keep-Alive");
conn.setDoInput(true);
conn.setDoOutput(true);
BufferedOutputStream hurlBufOus = new BufferedOutputStream(conn.getOutputStream());
String str = null;
/* if (status==0){//消耗
// 拼成固定的格式传给平台
str = String.format(Locale.CHINA, "{\"receipt-data\":\"" + receipt + "\"}");//消耗型内购
}else if (status == 1){*/
//连续包月订阅需要加上共享密钥
str = String.format(Locale.CHINA, "{\"receipt-data\":\"" + receipt + "\",\"password\":\"" + IOS_SHARED_SECRET_PASSWORD + "\"}");
//}
hurlBufOus.write(str.getBytes());
hurlBufOus.flush();

InputStream is = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line = null;
StringBuffer sb = new StringBuffer();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} catch (Exception ex) {
System.out.println("苹果服务器异常");
ex.printStackTrace();
}
return null;
}

/**
* 用BASE64加密
*
* @param str
* @return
*/
public static String getBASE64(String str) {
byte[] b = str.getBytes();
String s = null;
if (b != null) {
s = new sun.misc.BASE64Encoder().encode(b);
//s = new String(Base64.encodeBase64(b));
}
return s;
}
}

OrderController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
@PostMapping("/iosPaymentVerify")
@Authorization
@ApiOperation("苹果内购支付")
public Result<Object> iosPayment (@RequestBody Map<String,String> map, @CurrentUser User user){
//notice: 处理订阅类型的订单 in_app是一个数组列表 购买会员
String no = map.get("no");
String transactionId = map.get("transactionId");
String types = map.get("type");
int type = Integer.valueOf(types.trim()); //0消耗型 1订阅型
String payload = map.get("payload");
log.info("交易号:{},receipt:{},交易类型:{},本地订单号:{}", transactionId, payload,type,no);
//先校对与本地订单
Order localOrder = orderRepository.findByNo(no);//禁止重复刷单
if (localOrder == null) {
return new ResultUtil<>().setErrorMsg(this.i18n("this.order.does.not.exist.and.the.recharge.failed"));
}else {//防止重复刷单
if (localOrder.getStatus()==1){
return new ResultUtil<>().setErrorMsg(this.i18n("this.order.has.been.successfully.recharged.and.cannot.be.refilled"));
}

}
//防止重复刷单
Order lco = orderRepository.findByTransactionId(transactionId);
if (lco!=null&&lco.getStatus()==Constant.PAY_SUCCESS){
return new ResultUtil<>().setErrorMsg(this.i18n("this.order.has.been.successfully.recharged.and.cannot.be.refilled"));
}
String verifyResult = null;
String receipt = payload.replaceAll("%2B", "+"); //notice: 注意%2B的符号
try {
verifyResult = IosVerifyUtil.buyAppVerify(receipt, 1, type);//1.先线上测试,发送平台验证
} catch (Exception e1) {
throw new HttpRequestException(HttpServletResponse.SC_NOT_ACCEPTABLE, this.i18n("server.exception"));//服务器异常
}
if (verifyResult == null) {// 苹果服务器没有返回验证结果
throw new HttpRequestException(HttpServletResponse.SC_NOT_ACCEPTABLE, this.i18n("order.information.is.abnormal"));//订单信息异常
} else {// 苹果验证有返回结果
log.info("线上,苹果平台返回JSON:{}", verifyResult);
JSONObject job = JSONObject.parseObject(verifyResult);
String states = job.getString("status");
if ("21007".equals(states)) {//是沙盒环境,应沙盒测试,否则执行下面
try {
verifyResult = IosVerifyUtil.buyAppVerify(receipt, 0, type);//2.再沙盒测试 发送平台验证
} catch (Exception e) {
throw new HttpRequestException(HttpServletResponse.SC_NOT_ACCEPTABLE, this.i18n("server.exception"));//服务器异常
}
job = JSONObject.parseObject(verifyResult);
states = job.getString("status");
}
if (states.equals("0")) {// 前端所提供的收据是有效的,验证成功
String r_receipt = job.getString("receipt");
JSONObject returnJson = JSONObject.parseObject(r_receipt);
String in_app = returnJson.getString("in_app");
JSONArray arr = JSONArray.parseArray(in_app);//数组
boolean flag= false;
//如果单号一致 则保存到数据库
try {
for (int i = 0;i<arr.size();i++){
JSONObject obj = (JSONObject) arr.get(i);
// 交易中有当前交易,则认为交易成功
if (transactionId.equals(obj.get("transaction_id"))) {
flag = true;
}
if (flag) {//处理业务逻辑
String productId = obj.getString("product_id");
//Product product = productService.getByProductId(productId);
Product product = (Product) redisTemplate.opsForHash().get(Constant.Redis_Product+":"+productId,productId+"");
if (type == 0){//消耗型
int amount = product.getAmount();//充值的coin
user.setCoin(user.getCoin()+amount);
userService.updateUser(user);//给用户加币
localOrder.setType(type);//消耗型
localOrder.setStatus(Constant.PAY_SUCCESS);
localOrder.setTransactionId(transactionId);
localOrder.setProductId(product.getId());
localOrder.setQuantity(amount);
orderService.updateOrder(localOrder);
return new ResultUtil<>().setData(user.getCoin());
}else{//订阅型
return iosSubscribe(user, transactionId, type, localOrder, job, obj, productId, product);
//return iosSubscribe(user, transactionId, type, localOrder, obj, productId, product);
}
}
}
} catch (Exception e) {
throw new HttpRequestException(HttpServletResponse.SC_NOT_ACCEPTABLE, this.i18n("recharge.failed"));//充值失败
}
} else {
throw new HttpRequestException(HttpServletResponse.SC_NOT_ACCEPTABLE, this.i18n("invalid.receipt.information"));//收据信息异常
}
}
return new ResultUtil<>().setErrorMsg(this.i18n("there.is.a.problem.with.the.order.verification"));
}

private Result<Object> iosSubscribe(@CurrentUser User user, String transactionId, int type, Order localOrder, JSONObject job, JSONObject obj, String productId, Product product) {
//todo::首次订阅与连续性订阅的区别判断
String originTransactionId = obj.getString("original_transaction_id");//现交易中的原始订单号,作为续费判定的依据
long purchase_date = obj.getLong("purchase_date_ms");
long expires_date = obj.getLong("expires_date_ms");
Date expiresTime = new Date(expires_date);
boolean is_trial_period = true;//是否处于试用期间
String isTrial = obj.getString("is_trial_period");
is_trial_period = Boolean.parseBoolean(isTrial);
String iosMember = (String) redisTemplate.opsForValue().get(Constant.Redis_IMember);
String iosSuperMember = (String) redisTemplate.opsForValue().get(Constant.Redis_ISMember);
if (is_trial_period){//免费试用期间
if (productId.equals(iosMember)){//强者会员
user.setType(1);
}else if (productId.equals(iosSuperMember)){//超强者会员
user.setType(2);
}
Date joinTime = new Date(purchase_date);
user.setJoinTime(joinTime);//会员加入时间
user.setExpireTime(expiresTime);//会员过期时间
}else{//续费订阅,之前购买记录
//解析出 latest_receipt_info 中最新的一笔交易, 使用 expires_date_ms (过期时间)与当前时间作比较, 如果 expires_date_ms <当前时间, 则续费成功
if(!StringUtils.isEmpty(job.getString("latest_receipt_info"))){
JSONArray latestReceipt = JSONArray.parseArray(job.getString("latest_receipt_info"));//数组
JSONArray arrays = new JSONArray();
for (int j=0;j<latestReceipt.size();j++){
JSONObject latest = (JSONObject) latestReceipt.get(j);
if (originTransactionId.equals(latest.getString("original_transaction_id"))){
arrays.add(latest);
}
}
if (arrays.size()>0){
JSONObject lastobj = (JSONObject) arrays.get(arrays.size()-1);//最后一个元素是最新的续费数据
long expires_date_ms = lastobj.getLong("expires_date_ms");
Date expiresDate = new Date(expires_date_ms);
if (expiresDate.before(new Date())){//续费成功
user.setExpireTime(expiresTime);//更新会员过期时间
}
}
}
if (!StringUtils.isEmpty(job.getString("pending_renewal_info"))){//引用过去已打开或失败的自动更新订阅续订的元素数组。
JSONArray renewals = JSONArray.parseArray(job.getString("pending_renewal_info"));
JSONArray pendings = new JSONArray();
for (int p=0;p<renewals.size();p++){
JSONObject renewal = (JSONObject) renewals.get(p);
if (originTransactionId.equals(renewal.getString("original_transaction_id"))){
pendings.add(renewal);
}
}
JSONObject renewal = (JSONObject) pendings.get(pendings.size()-1);
int auto_renew_status = Integer.parseInt(renewal.getString("auto_renew_status"));//自动续订订阅的当前续订状态。 “ 1”-订阅将在当前订阅期结束时续订。“ 0”-客户已关闭其订阅的自动续订。
if (auto_renew_status==0){//用户关闭了订阅
if (expiresTime.before(new Date())){//已过期
user.setType(Constant.USER_COMMON);
userService.updateUser(user);
return new ResultUtil<>().setErrorMsg(this.i18n("subscription.expires"));
}else{
//todo:: 发送一条消息,到期自动更新用户会员状态
}
}else if (auto_renew_status == 1){//对于扣费失败的用户, 苹果仍会尝试扣款60天, 解析 pending_renewal_info , auto_renew_status 为1并且 is_in_billing_retry_period 为1, 此时用户的状态并不能标记为已关闭, 而应该是扣费失败
if (!StringUtils.isEmpty(job.getString("latest_expired_receipt_info"))){
JSONArray expiresReceipt = JSONArray.parseArray(job.getString("latest_expired_receipt_info"));
JSONArray expirearr = new JSONArray();
for (int e=0;e<expiresReceipt.size();e++){
JSONObject expires = (JSONObject) expiresReceipt.get(e);//最后一个即最新一个过期数据
if (originTransactionId.equals(expires.getString("original_transaction_id"))){
expirearr.add(expires);
}
}
JSONObject expire = (JSONObject) expirearr.get(expirearr.size()-1);
String retry_period = expire.getString("is_in_billing_retry_period");
if (retry_period.equals("1")){//-App Store仍在尝试续订。
return new ResultUtil<>().setErrorMsg(this.i18n("deduction.failed"));//扣费失败
}
}
}
}
}
userService.updateUser(user);
localOrder.setProductId(product.getId());
localOrder.setTransactionId(transactionId);
localOrder.setType(type);
localOrder.setStatus(Constant.PAY_SUCCESS);
orderService.updateOrder(localOrder);
//将ios回调信息记录在数据库,便于自动连续续订数据的后续校验
return new ResultUtil<>().setSuccessMsg(this.i18n("successfully"));
}


@Authorization
@PostMapping(value = "/createOrder")
@ApiOperation(value = "创建本地订单")
public Result<Object> createOrder(@RequestBody Order order, @CurrentUser User user) {
Order sysOrder = new Order();
sysOrder.setQuantity(order.getQuantity());
sysOrder.setPayType(order.getPayType());
sysOrder.setMoney(order.getMoney());
sysOrder.setType(order.getType());
sysOrder.setUser(user);
sysOrder.setStatus(Constant.PAY_FAILURE);
String no = String.valueOf(System.currentTimeMillis());//订单号
sysOrder.setNo(no + RandomUtil.getRandomNum());//时间戳+随机六位数
orderRepository.save(sysOrder);
return new ResultUtil<>().setData(sysOrder.getNo());
}

示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@PostMapping("/user/api/iosPaymentVerify")
@ApiOperation("苹果内购支付")
public Map<String, Object> iosPayment(HttpServletRequest request, @RequestBody
JSONObject jsonObject) throws Exception {
String payload = jsonObject.getString("payload");
String transactionId = jsonObject.getString("transactionId");
String access_token = request.getParameter("access_token");
//String transactionId = request.getParameter("transactionId");
StsUser user =this.loginUser();
Map<String, Object> map = new HashMap<String, Object>();
System.out.println("客户端传过来的值1:"+transactionId+"客户端传过来的值2:"+payload);
String verifyResult = null;
String receipt = payload.replaceAll("%2B","+");
try {
verifyResult = IosVerifyUtil.buyAppVerify(receipt,1);//1.先线上测试,发送平台验证
} catch (Exception e1) {
throw new HttpRequestException(this.i18n("apple.server.exception"));//服务器异常
}
if (verifyResult == null) {// 苹果服务器没有返回验证结果
throw new HttpRequestException(this.i18n("order.information.error"));//订单信息异常
} else {// 苹果验证有返回结果
System.out.println("线上,苹果平台返回JSON:"+verifyResult);
JSONObject job = JSONObject.parseObject(verifyResult);
String states = job.getString("status");
if("21007".equals(states)){//是沙盒环境,应沙盒测试,否则执行下面
try {
verifyResult = IosVerifyUtil.buyAppVerify(receipt,0);//2.再沙盒测试 发送平台验证
} catch (Exception e) {
throw new HttpRequestException(this.i18n("apple.server.exception"));//服务器异常
}
System.out.println("沙盒环境,苹果平台返回JSON:"+verifyResult);
job = JSONObject.parseObject(verifyResult);
states = job.getString("status");
}
System.out.println("苹果平台返回值:job"+job);
if (states.equals("0")){// 前端所提供的收据是有效的,验证成功
String r_receipt = job.getString("receipt");
JSONObject returnJson = JSONObject.parseObject(r_receipt);
String in_app = returnJson.getString("in_app");
JSONObject in_appJson = JSONObject.parseObject(in_app.substring(1, in_app.length()-1));
String product_id = in_appJson.getString("product_id");
String transaction_id = in_appJson.getString("transaction_id");// 订单号
String purchase_date = in_appJson.getString("purchase_date");
//如果单号一致 则保存到数据库
try {
if(transactionId.equals(transaction_id)){
String item = product_id.substring(product_id.length()-1);
int product_item = Integer.parseInt(item)-1;
OrderMoney[] orderMoney = sysConfiguration.getOrderConfig();//读取配置文件
int money = 0;//充值money
int diamond = 0;//充值的钻石数
for (int i = 0; i<orderMoney.length; i++){
if (product_item==i){
money = orderMoney[i].getMoney();
diamond = orderMoney[i].getDiamond();
}
}
long dia = user.getDiamond() + Long.valueOf(diamond);
payService.updateUser(user,dia);//update user
payService.saveOrder(user,diamond,transaction_id,money);//save order
map.put("result",true);
map.put("diamond",diamond);
map.put("message", "recharge.success");
}
} catch (Exception e) {
map.put("result", false);
map.put("message", "recharge.failure");
throw new HttpRequestException(this.i18n("recharge.failure"), e);//充值失败
}
} else {
map.put("result", false);
map.put("message", "receipt数据有问题");
throw new HttpRequestException(this.i18n("receipt.data.exception"));//收据信息异常
}
}
return map;
}

是不是感觉苹果内购简单多啦_,在这里还是要提醒一下诸位,苹果内购消耗型订单传过来的订单是单个,而订阅型传的是数组,上诉代码也可以看到。另外,苹果内购订阅型验证服务器需多加共享密钥(由ios开发人员提供给你)验证,而消耗型不需要。还要注意的是ios端传过来的苹果回调收据信息需base64加密,如果有严重呢个错误问题,注意收据信息的特殊符号替换,eg:"%2B"。

欣赏此文?求鼓励,求支持!