一、首先我们先简单理一下整个内购的核心流程:
①客户端发起支付订单
②客户端监听购买结果
③苹果回调订单购买成功时,客户端把苹果给的receipt_data和一些订单信息上报给服务器
④后台服务器拿receipt_data向苹果服务器校验
⑤苹果服务器向返回status结果,含义如下,其中为0时表示成功。
21000 App Store无法读取你提供的JSON数据
21002 收据数据不符合格式
21003 收据无法被验证
21004 你提供的共享密钥和账户的共享密钥不一致
21005 收据服务器当前不可用
21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
21008 收据信息是产品环境中使用,但却被发送到测试环境中验证
⑥服务器发现订单校验成功后,会把这笔订单存起来,transaction_id用MD5值映射下,保存到数据库,防止同一笔订单,多次发放内购商品。
苹果服务器返回给我们数据:
{
"environment": "Sandbox",
"receipt": {
"adam_id": 0,
"app_item_id": 0,
"application_version": "2.0.6",
"bundle_id": "com.appstoreMJB.mobao",
"download_id": 0,
"in_app": [{
"is_trial_period": "false",
"original_purchase_date": "2019-10-25 01:07:14 Etc/GMT",
"original_purchase_date_ms": "1571965634000",
"original_purchase_date_pst": "2019-10-24 18:07:14 America/Los_Angeles",
"original_transaction_id": "1000000583857816",
"product_id": "com.wha.***.6",
"purchase_date": "2019-10-25 01:07:14 Etc/GMT",
"purchase_date_ms": "1571965634000",
"purchase_date_pst": "2019-10-24 18:07:14 America/Los_Angeles",
"quantity": "1",
"transaction_id": "1000000583857816"
}],
"original_application_version": "1.0",
"original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
"original_purchase_date_ms": "1375340400000",
"original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
"receipt_creation_date": "2019-10-25 01:07:14 Etc/GMT",
"receipt_creation_date_ms": "1571965634000",
"receipt_creation_date_pst": "2019-10-24 18:07:14 America/Los_Angeles",
"receipt_type": "ProductionSandbox",
"request_date": "2019-10-25 01:07:16 Etc/GMT",
"request_date_ms": "1571965636457",
"request_date_pst": "2019-10-24 18:07:16 America/Los_Angeles",
"version_external_identifier": 0
},
"status": 0
}
验证代码:
package com.wha.controller;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.wha.dao.RecRechargeMapper;
import com.wha.model.RecRecharge;
import com.wha.model.sys.RequestLog;
import com.wha.service.UserService;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.wha.model.ReturnData;
import com.wha.service.PayService;
import com.wha.util.CommonUtils;
@Controller
@RequestMapping("iap")
public class IapController {
private Logger logger = Logger.getLogger(this.getClass());
@Autowired
private PayService payService;
@Autowired
private RecRechargeMapper recRechargeMapper;
// 正式 购买凭证验证地址
private static final String certificateUrl = "https://buy.itunes.apple.com/verifyReceipt";
// 沙箱 购买凭证验证地址
private static final String certificateUrlTest = "https://sandbox.itunes.apple.com/verifyReceipt";
/**
* 接收iOS端发过来的购买凭证
*
* @param userId
* @param certificateCode
* @param
* @throws IOException
*/
@RequestMapping("/setIapCertificate")
public void setIapCertificate(String userId, String certificateCode , HttpServletResponse response, HttpServletRequest request) throws IOException {
if (StringUtils.isEmpty(userId) || StringUtils.isEmpty(certificateCode )) {
CommonUtils.returnCode(response, new ReturnData(1, "缺少参数", null));
return;
}
String url = certificateUrl;
try {
String sendHttpsCoon = sendHttpsCoon(url, certificateCode);
JSONObject json = JSONObject.parseObject(sendHttpsCoon);
if ("21007".equals(json.get("status").toString())) {
url = certificateUrlTest;
sendHttpsCoon = sendHttpsCoon(url, certificateCode); //发送请求
}
JSONObject jsonObject = JSONObject.parseObject(sendHttpsCoon);
if ("0".equals(jsonObject.get("status").toString())) { //苹果服务器向返回status结果
JSONArray inapp = (JSONArray) ((JSONObject) jsonObject.get("receipt")).get("in_app");
if (inapp.size() > 0) { //如果订单状态成功在判断in_app这个字段有没有,没有直接就返回失败了。如果存在的话,遍历整个数组,通过客户端给的transaction_id 来比较
JSONObject parseObject = (JSONObject) inapp.get(0);
String product_id = parseObject.get("product_id").toString();
String transaction_id = parseObject.get("transaction_id").toString();
RecRecharge recRecharge = recRechargeMapper.selectByTransactionId(transaction_id);
if (!CommonUtils.isEmptyString(product_id) && !CommonUtils.isEmptyString(transaction_id) && null == recRecharge) {//判重,避免重复分发内购商品。收到客户端上报的transaction_id后,直接MD5后去数据库查,能查到说明是重复订单就不做处理
String[] split = product_id.split("com.wha.***."); //获取订单价格
payService.payReturn("A" + transaction_id, userId, split[1]);//处理自己的逻辑,将transaction_id存入数据库,完成订单
}
}
}
CommonUtils.returnCode(response, new ReturnData(0, "ok", jsonObject));
} catch (Exception e) {
logger.error("购买凭证验证错误", e);
CommonUtils.returnCode(response, new ReturnData(1, "购买凭证验证错误", null));
}
}
/**
* 重写X509TrustManager
*/
private static TrustManager myX509TrustManager = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
};
/**
* 发送请求
*
* @param url
* @param
* @return
*/
private String sendHttpsCoon(String url, String code) {
if (url.isEmpty()) {
return null;
}
try {
// 设置SSLContext
SSLContext ssl = SSLContext.getInstance("SSL");
ssl.init(null, new TrustManager[]{myX509TrustManager}, null);
// 打开连接
HttpsURLConnection conn = (HttpsURLConnection) new URL(url).openConnection();
// 设置套接工厂
conn.setSSLSocketFactory(ssl.getSocketFactory());
// 加入数据
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-type", "application/json");
JSONObject obj = new JSONObject();
obj.put("receipt-data", code);
BufferedOutputStream buffOutStr = new BufferedOutputStream(conn.getOutputStream());
buffOutStr.write(obj.toString().getBytes());
buffOutStr.flush();
buffOutStr.close();
// 获取输入流
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = null;
StringBuffer sb = new StringBuffer();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} catch (Exception e) {
return null;
}
}
}
丢单:
引起内购丢单的主要操作其实是当用户点击内购商品时,苹果服务器太慢了,支付页面一直不出来。结果用户退出或者杀死App,这时候在Home页面,支付框又弹出来了,然后用户点击支付,成功后在打开App发现丢单。
一般这种只要你在Appdelegate的didFinishLaunchingWithOptions方法就开始对苹果内购回调做监听,然后把所有相关内购的东西抽出来做一个单例即可解决丢单