最近公司开发APP需要做IOS的支付功能,所以研究了一下IOS的支付,IOS支付和国内的微信支付宝支付流程有点不一样,IOS支付成功后也是依赖IOS APP回调的后台服务器,回调时携带IOS支付成功后的支付凭证等信息给服务器,不像微信支付宝是使用的他们的服务器通过REST API的方式回调,这可能和苹果是全球跨国企业,但有的国家对隐私做的比较严,不想暴露服务器的地址,所以才采用的这种方式,下面直接上代码。
1,开发前需要ios开发人员添加内购项目,如下:
IOS支付金额是固定的,不像微信支付宝那样可以随意输入支付金额,并且需要先在ios开发者后台添加配置好内购项目,参考名称和产品ID,可以设置成一样,那个也就是配置好的支付金额。
2,将配置好的内购项目转换成JSON格式,通过接口返回给IOS开发人员,我的格式定义如下:
[
{
"amount": 208,
"productId": "208",
"productName": "208"
},
{
"amount": 518,
"productId": "518",
"productName": "518"
},
{
"amount": 1098,
"productId": "1098",
"productName": "1098"
},
{
"amount": 2298,
"productId": "2298",
"productName": "2298"
},
{
"amount": 3298,
"productId": "3298",
"productName": "3298"
},
{
"amount": 4498,
"productId": "4498",
"productName": "4498"
}
]
3,IOS支付成功后通过APP回调服务器,完成整个的支付操作:
里面的服务类和具体的业务流程需要自己写,这里只贴出了ios验证的流程
@ApiOperation("ios支付成功后验证结果")
@RequestMapping(value = "/iPayNotify", method = RequestMethod.POST)
@ResponseBody
public ResultPageVo iPayNotify(@RequestBody IPayNotifyPo iPayNotifyPo) {
log.debug("前段传递的ios支付参数=" + iPayNotifyPo.toString());
String receipt = iPayNotifyPo.getTransactionReceipt();
// 拿到收据的MD5
String receiptMd5 = SecureUtil.md5(receipt);
// 查询数据库,看是否是己经验证过的该支付收据
boolean existsIOSReceipt = userOrderService.isExistsIOSReceipt(receiptMd5);
if (existsIOSReceipt) {
return ResultPageVo.failure("该充值已完成");
}
// 1.先线上测试 发送平台验证
String verifyResult = IosVerifyUtil.buyAppVerify(receipt, 1);
log.debug("1,苹果返回的参数=" + verifyResult);
if (verifyResult == null) {
// 苹果服务器没有返回验证结果
log.debug("苹果服务器没有返回验证结果");
return ResultPageVo.failure("订单没有找到");
} else {
// 苹果验证有返回结果
JSONObject job = JSONUtil.parseObj(verifyResult);
log.debug("2,苹果验证返回的json串=" + job.toString());
String states = job.getStr("status");
if ("21007".equals(states)) {
log.debug("是沙盒环境,应沙盒测试,否则执行下面");
// 是沙盒环境,应沙盒测试,否则执行下面
// 2.再沙盒测试 发送平台验证
verifyResult = IosVerifyUtil.buyAppVerify(receipt, 0);
job = JSONUtil.parseObj(verifyResult);
log.debug("3,沙盒环境验证返回的json字符串=" + job.toString());
states = job.getStr("status");
}
if ("0".equals(states)) { // 前端所提供的收据是有效的 验证成功
log.debug("前端所提供的收据是有效的 验证成功");
String r_receipt = job.getStr("receipt");
JSONObject returnJson = JSONUtil.parseObj(r_receipt);
String in_app = returnJson.getStr("in_app");
/**
* in_app说明:
* 验证票据返回的receipt里面的in_app字段,这个字段包含了所有你未完成交易的票据信息。也就是在上面说到的APP完成交易之后,这个票据信息,就会从in_app中消失。
* 如果APP不完成交易,这个票据信息就会在in_app中一直保留。(这个情况可能仅限于你的商品类型为消耗型)
*
* 知道了事件的原委,就很好优化解决了,方案有2个
* 1.对票据返回的in_app数据全部进行处理,没有充值的全部进行充值
* 2.仅对最新的充值信息进行处理(我们采取的方案)
*
* 因为采用一方案:
* 如果用户仅进行了一次充值,该充值未到账,他不再进行充值了,那么会无法导致。
* 如果他通过客服的途径已经进行了补充充值,那么他在下一次充值的时候依旧会把之前的产品票据带回,这时候有可能出现重复充值的情况
*
* 以上说明是我在网上找到的,可以查看原文
*
*/
JSONArray jsonArray = JSONUtil.parseArray(in_app);
if (jsonArray.size() > 0) {
int index = 0;
JSONObject o = JSONUtil.parseObj(jsonArray.get(index));
String transaction_id = o.getStr("transaction_id"); // 订单号
String product_id = o.getStr("product_id"); // 产品id,也就是支付金额
String purchase_date_ms = o.getStr("purchase_date_ms"); // 支付时间
// 添加支付金额
userOrderService.iosChargeSuccess(transaction_id, product_id, purchase_date_ms, receiptMd5);
}
} else {
return ResultPageVo.failure("收到数据有误");
}
}
return ResultPageVo.success();
}
Po参数封装类:
@Data
public class IPayNotifyPo {
@ApiModelProperty("苹果支付凭证")
private String transactionReceipt;
@ApiModelProperty("苹果支付单号")
private String payId;
@ApiModelProperty("用户id")
private Integer userId;
}
IosVerifyUtil 工具类:
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;
/**
* @desc: 苹果IAP内购验证工具类
* @author: hwm
* @date: 2019/9/3 17:11
*/
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";
/**
* 苹果服务器验证
*
* @param receipt 账单
* @return null 或返回结果 沙盒 https://sandbox.itunes.apple.com/verifyReceipt
* @url 要验证的地址
*/
public static String buyAppVerify(String receipt, int type) {
//环境判断 线上/开发环境用不同的请求链接
String url = "";
if (type == 0) {
url = url_sandbox; //沙盒测试
} else {
url = url_verify; //线上测试
}
//String url = EnvUtils.isOnline() ?url_verify : url_sandbox;
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("Content-Type", "application/json");
conn.setRequestProperty("Proxy-Connection", "Keep-Alive");
conn.setDoInput(true);
conn.setDoOutput(true);
BufferedOutputStream hurlBufOus = new BufferedOutputStream(conn.getOutputStream());
String str = String.format(Locale.CHINA, "{\"receipt-data\":\"" + receipt + "\"}");//拼成固定的格式传给平台
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);
}
return s;
}
}
使用到的JSON工具类pom导入,也可以改成自己项目里面的工具类稍微改下就好了
<!-- 工具包:https://hutool.cn/docs/ -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.1</version>
</dependency>
最后贴一段沙盒环境凭证验证成功的json数据结果:
{
"environment": "Sandbox",
"receipt": {
"in_app": [
{
"transaction_id": "1000000564723363",
"original_purchase_date": "2019-09-04 11:51:19 Etc/GMT",
"quantity": "1",
"original_transaction_id": "1000000564723363",
"purchase_date_pst": "2019-09-04 04:51:19 America/Los_Angeles",
"original_purchase_date_ms": "1567597879000",
"purchase_date_ms": "1567597879000",
"product_id": "208",
"original_purchase_date_pst": "2019-09-04 04:51:19 America/Los_Angeles",
"is_trial_period": "false",
"purchase_date": "2019-09-04 11:51:19 Etc/GMT"
}
],
"adam_id": 0,
"receipt_creation_date": "2019-09-04 11:51:19 Etc/GMT",
"original_application_version": "1.0",
"app_item_id": 0,
"original_purchase_date_ms": "1375340400000",
"request_date_ms": "1567597892412",
"original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
"original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
"receipt_creation_date_pst": "2019-09-04 04:51:19 America/Los_Angeles",
"receipt_type": "ProductionSandbox",
"bundle_id": "com.Lanhu.project.SixPigeonClass",
"receipt_creation_date_ms": "1567597879000",
"request_date": "2019-09-04 11:51:32 Etc/GMT",
"version_external_identifier": 0,
"request_date_pst": "2019-09-04 04:51:32 America/Los_Angeles",
"download_id": 0,
"application_version": "1"
},
"status": 0
}