提示: 近期公司业务中APP新增了苹果内购支付,首次对接IOS支付,在此做记录。简单研究后发现,IOS支付和国内的微信支付宝支付流程有点不一样,IOS支付成功后也是依赖IOS APP客户端回调后台服务器,回调时携带IOS支付成功后的支付凭证等信息给服务器,跟微信支付宝不同的是微信和支付宝是通过他们的服务器通过REST API的方式回调,这可能和苹果是全球跨国企业,但有的国家对隐私做的比较严,不想暴露服务器的地址,所以才采用的这种方式,下面直接上代码
注意:ios支付的金额是固定金额,跟支付宝微信不同的是,微信支付宝可以随意的输入金额。ios开发人员需要在苹果内购系统中配置好内购项目。
后端也要在数据库中配置跟ios内购项目相同的东西,以便在开发过程中能跟ios统一,结果如下:
{
"id": "1",
"product_id": "001",
"rmb_price": 1,
},
{
"id": "2",
"product_id": "002",
"rmb_price": 6,
},
{
"id": "3",
"product_id": "003",
"rmb_price": 12,
}
注释:里边可能会涉及一些业务 需要自己来更改
/**
* IOS支付成功后通过APP回调服务器
*
* @param iPayNotifyPo
* @return
*/
@ApiOperation("ios支付成功后验证结果")
@RequestMapping(value = "iPayNotify/", method = RequestMethod.POST)
@ResponseBody
public Result<Object> iPayNotify(@RequestBody IPayNotifyPo iPayNotifyPo) {
logger.info("ios支付成功后验证结果[前端传递的ios支付参数:{}]", iPayNotifyPo.toString());
String receipt = iPayNotifyPo.getTransactionReceipt();
// 拿到收据的MD5
String receiptMd5 = SecureUtil.md5(receipt);
// 查询数据库,看是否是己经验证过的该支付收据
boolean existsIOSReceipt = paymentService.isExistsIOSReceipt(receiptMd5);
if (existsIOSReceipt) {
return Result.error("该充值已完成");
}
// 1.先线上测试 发送平台验证
String verifyResult = IosVerifyUtil.buyAppVerify(receipt, 1);
logger.info("1,苹果返回的参数:{}]", verifyResult);
if (verifyResult == null) {
// 苹果服务器没有返回验证结果
logger.info("苹果服务器没有返回验证结果");
return Result.error("订单没有找到");
} else {
// 苹果验证有返回结果
JSONObject job = JSONUtil.parseObj(verifyResult);
logger.info("2,[苹果验证返回的json串:{}]", job.toString());
String states = job.getStr("status");
if ("21007".equals(states)) {
logger.debug("是沙盒环境,应沙盒测试,否则执行下面");
// 是沙盒环境,应沙盒测试,否则执行下面
// 2.再沙盒测试 发送平台验证
verifyResult = IosVerifyUtil.buyAppVerify(receipt, 0);
job = JSONUtil.parseObj(verifyResult);
logger.debug("3,沙盒环境验证返回的json字符串=" + job.toString());
states = job.getStr("status");
}
if ("0".equals(states)) { // 前端所提供的收据是有效的 验证成功
logger.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.仅对最新的充值信息进行处理(我们采取的方案)
*
* 因为采用一方案:
* 如果用户仅进行了一次充值,该充值未到账,他不再进行充值了,那么会无法导致。
* 如果他通过客服的途径已经进行了补充充值,那么他在下一次充值的时候依旧会把之前的产品票据带回,这时候有可能出现重复充值的情况
*
* 以上说明是我在网上找到的,可以查看原文
* https://www.cnblogs.com/widgetbox/p/8241333.html
*/
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"); // 支付时间
/**
* 此处为业务代码,可以根据自己的业务来进行开发
*
*/
// 添加支付金额
Result<Object> iosChargeSuccess = paymentService.iosChargeSuccess(transaction_id, product_id, purchase_date_ms, receiptMd5, iPayNotifyPo.getMoneyId(), iPayNotifyPo.getUserId());
return iosChargeSuccess;
}
} else {
return Result.error("收到数据有误");
}
}
return Result.ok();
}
里边涉及到的业务,可以根据自己的需求来进行开发
/**
* 看是否是己经验证过的该支付收据
* @param receiptMd5
* @return
*/
public boolean isExistsIOSReceipt(String receiptMd5);
/**
* IOS支付
* @param transaction_id 订单号
* @param product_id 产品id,也就是支付金额
* @param purchase_date_ms 支付时间
* @param receiptMd5 拿到收据的MD5
* @param moneyId 金额id
*/
public Result<Object> iosChargeSuccess(String transaction_id, String product_id, String purchase_date_ms, String receiptMd5,String moneyId,String userId);
public class IPayNotifyPo {
@ApiModelProperty("苹果支付凭证")
private String transactionReceipt;
@ApiModelProperty("苹果支付单号")
private String payId;
@ApiModelProperty("用户id")
private String userId;
@ApiModelProperty("金额id")
private String moneyId;
}
package com.tools.payment.ios;
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: wyq
* @date: 2022/07/25 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;
}
}
package com.tools.result;
import java.util.List;
import java.util.Map;
public class Result<T> {
/**
* 成功标志
*/
private boolean success = true;
/**
* 返回代码
*/
private Integer code = 0;
/**
* 返回处理消息
*/
private String message = "操作成功!";
/**
* 返回数据对象 result
*/
private T result;
/**
* 时间戳
*/
private long timestamp = System.currentTimeMillis();
public Result() {
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getResult() {
return result;
}
public void setResult(T result) {
this.result = result;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public Result<T> error500(String message) {
this.setMessage(message);
this.setCode(500);
this.setSuccess(false);
return this;
}
public Result<T> success(String message) {
this.setMessage(message);
this.setCode(200);
this.setSuccess(true);
return this;
}
public static Result<Object> ok() {
Result<Object> r = new Result<Object>();
r.setMessage("操作成功");
r.setCode(200);
r.setSuccess(true);
return r;
}
public static Result<Object> ok(String msg) {
Result<Object> r = new Result<Object>();
r.setMessage(msg);
r.setCode(200);
r.setSuccess(true);
r.setResult(null);
return r;
}
public static Result<Object> ok(Map data) {
Result<Object> r = new Result<Object>();
r.setCode(200);
r.setMessage("操作成功");
r.setSuccess(true);
r.setResult(data);
return r;
}
public static Result<Object> ok(Map data,String msg) {
Result<Object> r = new Result<Object>();
r.setCode(200);
r.setMessage(msg);
r.setSuccess(true);
r.setResult(data);
return r;
}
public static Result<Object> ok(List data) {
Result<Object> r = new Result<Object>();
r.setCode(200);
r.setSuccess(true);
r.setResult(data);
return r;
}
public static Result<List<?>> okl(List<?> data) {
Result<List<?>> r = new Result<List<?>>();
r.setCode(200);
r.setSuccess(true);
r.setResult(data);
return r;
}
public static Result<Object> ok(Object data) {
Result<Object> r = new Result<Object>();
r.setCode(200);
r.setSuccess(true);
r.setResult(data);
return r;
}
public static Result<Object> error(String msg) {
return error(403, msg);
}
public static Result<Object> error(int code, String msg) {
Result<Object> r = new Result<Object>();
r.setCode(code);
r.setSuccess(false);
r.setMessage(msg);
return r;
}
public static Result<Object> error(Map data,String msg) {
Result<Object> r = new Result<Object>();
r.setCode(403);
r.setMessage(msg);
r.setSuccess(false);
r.setResult(data);
return r;
}
/**
* 无权限访问返回结果
*/
public static Result<Object> noauth(String msg) {
return error(401, msg);
}
/**
* 无权限访问返回结果
*/
public static Result<List<?>> noauth() {
Result<List<?>> r = new Result<List<?>>();
r.setCode(401);
r.setSuccess(false);
r.setMessage("您没有该接口的权限!");
return r;
}
}
<!-- 工具包:https://hutool.cn/docs/ -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.1</version>
</dependency>