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
114
115
116
package com.xy.goone.common.util.pay;

import com.google.api.client.util.Base64;

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
package com.xy.goone.modules.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.androidpublisher.AndroidPublisher;
import com.google.api.services.androidpublisher.model.ProductPurchase;
import com.google.api.services.androidpublisher.model.ProductPurchasesAcknowledgeRequest;
import com.google.api.services.androidpublisher.model.SubscriptionPurchase;
import com.google.api.services.androidpublisher.model.SubscriptionPurchasesAcknowledgeRequest;
import com.xy.goone.auth.annotation.Authorization;
import com.xy.goone.auth.annotation.CurrentUser;
import com.xy.goone.common.Constant;
import com.xy.goone.common.exception.HttpRequestException;
import com.xy.goone.common.util.HttpUtils;
import com.xy.goone.common.util.JsonUtil;
import com.xy.goone.common.util.RandomUtil;
import com.xy.goone.common.util.ResultUtil;
import com.xy.goone.common.util.pay.IosVerifyUtil;
import com.xy.goone.common.vo.Result;
import com.xy.goone.config.pay.GooglePayConfig;
import com.xy.goone.modules.dao.OrderRepository;
import com.xy.goone.modules.domain.Order;
import com.xy.goone.modules.domain.User;
import com.xy.goone.modules.model.Pay;
import com.xy.goone.modules.model.Product;
import com.xy.goone.modules.service.OrderService;
import com.xy.goone.modules.service.ProductService;
import com.xy.goone.modules.service.UserService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/***
* @author fumei.jiang
* @date 2019-08-09 14:59
*/
@RestController
@Slf4j
@RequestMapping(value = "/order")
public class OrderController extends BaseController{

@Resource
OrderService orderService;

@Resource
UserService userService;

@Resource
OrderRepository orderRepository;

@Resource
GooglePayConfig googlePayConfig;

@Resource
ProductService productService;

private static Map<String, String> cacheToken = null;//设置静态变量,用于判断access_token是否过期

@PostMapping("/iosPaymentVerify")
@Authorization
@ApiOperation("苹果内购支付")
//todo:: 消耗性商品 订单一条 订阅型商品(购买会员)传的订单是多条
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");
Integer type = Integer.valueOf(map.get("type")); //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"));
}
if (localOrder.getStatus() == Constant.PAY_SUCCESS && localOrder.getTransactionId().equals(transactionId)) {
return new ResultUtil<>().setErrorMsg(this.i18n("this.order.has.been.successfully.recharged.and.cannot.be.refilled"));
}
String verifyResult = null;
String receipt = payload.replaceAll("%2B", "+"); //todo: 注意%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"));//服务器异常
}
log.info("沙盒环境,苹果平台返回JSON:", verifyResult);
job = JSONObject.parseObject(verifyResult);
states = job.getString("status");
}
log.info("苹果平台返回值: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");
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((String) 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+"");
BigDecimal money = product.getPrice();//充值money
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{//订阅型
long purchase_date = obj.getLong("purchase_date_ms");
long expires_date = obj.getLong("expires_date_ms");
boolean is_trial_period = true;//是否处于试用期间
String isTrial = obj.getString("is_trial_period");
user.setJoinTime(new Date(purchase_date));//会员加入时间
user.setExpireTime(new Date(expires_date));//会员过期时间
is_trial_period = Boolean.parseBoolean(isTrial);
if (is_trial_period||productId.equals("com.kasege.RZ.vip.980")){//强者会员
user.setType(1);
} else if (productId.equals("com.kasege.RZ.vip.10400")){//超强者会员
user.setType(2);
}
userService.updateUser(user);
localOrder.setProductId(product.getId());
localOrder.setTransactionId(transactionId);
localOrder.setType(type);
localOrder.setStatus(Constant.PAY_SUCCESS);
orderService.updateOrder(localOrder);
return new ResultUtil<>().setSuccessMsg(this.i18n("successfully"));
}
}
}
} 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"));
}

@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"。

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