使用ios内购,需在项目数据库建立虚拟币相关表(虚拟币余额表、充值面额表、充值订单表等)上代码
苹果IAP内购验证工具类 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; //线上测试
}
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 = 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;
}
}
定义api入参
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("apple支付表单信息")
public class IPayNotifyFrom {
/**
* 票据
*/
@ApiModelProperty("票据")
private String payload;
/**
* 订单id 充值订单表
*/
@ApiModelProperty("订单id")
private String payOrderId;
}
定义接口
import com.szylt.kidsays.project.manager.entity.AppNotifications;
import com.szylt.kidsays.project.vo.ResultVo;
import com.szylt.kidsays.project.form.apple.IPayNotifyFrom;
import com.szylt.kidsays.project.vo.apple.JsonResult;
import org.springframework.http.ResponseEntity;
/**
* ios支付接口
*/
public interface IosPayService {
/**
* ios充值支付成功后验证结果
* @param iPayNotifyVO
* @param uId
* @return
*/
ResponseEntity iosPay(IPayNotifyFrom iPayNotifyVO,String uId);
/**
* ios购买结账
* @param uId
* @param orderId
* @return
*/
ResultVo iosTheInvoicing(String uId, String orderId);
/**
* ios服务通知
* @param notifications
* @return
*/
void notification(AppNotifications notifications);
}
实现接口
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.szylt.kidsays.common.config.utils.ResultVoUtil;
import com.szylt.kidsays.common.constant.ResultCode;
import com.szylt.kidsays.project.entity.HOrder;
import com.szylt.kidsays.project.manager.entity.AppNotifications;
import com.szylt.kidsays.project.manager.entity.HPayOrder;
import com.szylt.kidsays.project.manager.entity.Latest_Receipt_Info;
import com.szylt.kidsays.project.manager.service.IHPayOrderService;
import com.szylt.kidsays.project.manager.service.IHVirtualCoinService;
import com.szylt.kidsays.project.manager.service.IosPayService;
import com.szylt.kidsays.project.manager.config.util.IosVerifyUtil;
import com.szylt.kidsays.project.manager.vo.InvoicingVo;
import com.szylt.kidsays.project.service.IHOrderService;
import com.szylt.kidsays.project.vo.ResultVo;
import com.szylt.kidsays.project.form.apple.IPayNotifyFrom;
import com.szylt.kidsays.project.vo.apple.JsonResult;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
/**
* 支付业务接口实现
*/
@Slf4j
@Service
@AllArgsConstructor
public class IosPayServiceImpl implements IosPayService {
private static final Logger logger = LoggerFactory.getLogger("sys-pay");
private IHPayOrderService payOrderService;
private IHOrderService orderService;
private IHVirtualCoinService virtualCoinService;
/**
* ios充值支付成功后验证结果
* @param iPayNotifyVO
* @param uId
* @return
*/
@Override
public ResponseEntity iosPay(IPayNotifyFrom iPayNotifyVO, String uId) {
JsonResult r = new JsonResult();
if (uId == null) {
r.setCode("0");
r.setMessage("未登录");
return ResponseEntity.ok(r);
}
//线上环境验证 type值 为0的时候是 沙盒环境、1为 线上环境
String verifyResult = IosVerifyUtil.buyAppVerify(iPayNotifyVO.getPayload(), 1);
if (verifyResult == null) {
r.setCode("0");
r.setMessage("苹果验证失败,返回数据为空");
} else {
logger.info("线上,苹果平台返回JSON:" + verifyResult);
JSONObject appleReturn = JSONObject.parseObject(verifyResult);
String states = appleReturn.getString("status");
//无数据则沙箱环境验证
if ("21007".equals(states)) {
verifyResult = IosVerifyUtil.buyAppVerify(iPayNotifyVO.getPayload(), 0);
logger.info("沙盒环境,苹果平台返回JSON:" + verifyResult);
appleReturn = JSONObject.parseObject(verifyResult);
states = appleReturn.getString("status");
}
logger.info("苹果平台返回值:appleReturn" + appleReturn);
// 前端所提供的收据是有效的 验证成功
if (states.equals("0")) {
String receipt = appleReturn.getString("receipt");
JSONObject returnJson = JSONObject.parseObject(receipt);
String inApp = returnJson.getString("in_app");
String productId = null;
String transactionId = null;
List inApps = JSONObject.parseArray(inApp, HashMap.class);
if (!CollectionUtils.isEmpty(inApps)) {
ArrayList transactionIds = new ArrayList();
for (HashMap app : inApps) {
transactionIds.add((String) app.get("transaction_id"));
transactionId = (String) app.get("transaction_id");
productId = (String) app.get("product_id");
}
//交易列表包含当前交易,则 认为交易成功
if (transactionIds.contains(transactionId)) {
/**
* 处理业务 修改充值订单数据 并增加用户余额
*/
// boolean result = payOrderService.upPayOrder(uId,transactionId,productId,iPayNotifyVO.getPayOrderId());
// if (result){
// logger.info("交易成功,新增并处理订单:{}transactionId{}: " + transactionId);
// r.setCode("200");
// r.setMessage("充值成功");
// return ResponseEntity.ok(r);
// }
r.setCode("0");
r.setMessage("支付失败");
logger.info("支付失败,错误码:" + states);
return ResponseEntity.ok(r);
}
r.setCode("0");
r.setMessage("当前交易不在交易列表中");
return ResponseEntity.ok(r);
}
r.setCode("0");
r.setMessage("未能获取获取到交易列表");
} else {
r.setCode("0");
r.setMessage("支付失败");
logger.info("支付失败,错误码:" + states);
}
}
return ResponseEntity.ok(r);
}
/**
* ios/购买结账 减用户余额
* @param uId
* @param orderId
* @return
*/
@Transactional(rollbackFor = {Exception.class})
@Override
public ResultVo iosTheInvoicing(String uId, String orderId) {
InvoicingVo invoicingVo = new InvoicingVo();
boolean upOrderResult = orderService.updateOrder(orderId, "Kind Says币结算", 2);
//HPayOrder payOrder = payOrderService.queryPayOrder(orderId);
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("o_id",orderId);
HOrder order = orderService.getOne(wrapper);
BigDecimal balance = virtualCoinService.findVirtualCoin(uId);
if (balance.compareTo(order.getOrderPriceUsd()) == -1){
return ResultVoUtil.error(ResultCode.VIRTUAL_INSUFFICIENT_BALANCE,"Insufficient balance");
}
boolean result = virtualCoinService.subtraction(uId, String.valueOf(order.getOrderPriceUsd()));
if (result){
orderService.updateOrderPayment(orderId, null);
invoicingVo.setState("SUCCESS");
BigDecimal price = virtualCoinService.findVirtualCoin(uId);
invoicingVo.setBalance(price);
return ResultVoUtil.success(invoicingVo);
}
return ResultVoUtil.error(ResultCode.VIRTUAL_FAIL,"FAIL");
}
/**
* ios服务通知 修改充值订单 增加用户余额
* @param notifications
* @return
*/
@Override
public void notification(AppNotifications notifications) {
if (notifications.getNotification_type().equals("REFUND")){
for (Latest_Receipt_Info lr:notifications.getUnified_receipt().getLatest_receipt_info()) {
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("transaction_id",lr.getTransaction_id());
wrapper.eq("status",1);
HPayOrder order = payOrderService.getOne(wrapper);
if (Objects.isNull(order)){
logger.info("apple已进行过退款");
break;
}
boolean result = virtualCoinService.addition(order.getUId(), String.valueOf(order.getUId()));
if (!result){
logger.info("apple退款失败");
}
logger.info("apple退款成功");
}
}
}
}
定义api入口
import com.szylt.kidsays.login.authorization.BaseController;
import com.szylt.kidsays.project.manager.entity.AppNotifications;
import com.szylt.kidsays.project.manager.service.IosPayService;
import com.szylt.kidsays.project.vo.ResultVo;
import com.szylt.kidsays.project.form.apple.IPayNotifyFrom;
import com.szylt.kidsays.project.vo.apple.JsonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@RestController
@Api(tags = "Apple支付")
@Slf4j
@RequestMapping(value = "/apple")
@AllArgsConstructor
public class ApplePayController extends BaseController {
private static final Logger logger = LoggerFactory.getLogger("sys-pay");
private IosPayService iosPayService;
@RequiresAuthentication
@ApiOperation("ios充值支付成功后验证结果")
@PostMapping("/iospay")
public synchronized ResponseEntity iosPay(@RequestBody IPayNotifyFrom iPayNotifyVO, HttpServletRequest request) {
String uId = getTokenUid(request);
logger.info("ApplePayController.iosPay>>>>>>>>>>>>>>>>>>{iospay}apple充值支验证:IPayNotifyFrom=",iPayNotifyVO);
return iosPayService.iosPay(iPayNotifyVO,uId);
}
@RequiresAuthentication
@ApiOperation("ios购买结账")
@PostMapping("/checkout")
public ResultVo iosTheInvoicing(@ApiParam(name = "orderId",value = "订单号") @RequestParam(value = "orderId",required = false)String orderId,
HttpServletRequest request) {
String uId = getTokenUid(request);
return iosPayService.iosTheInvoicing(uId,orderId);
}
/**
* ios服务通知
*/
@ApiOperation(value = "ios服务通知")
@PostMapping("/notification")
@ResponseBody
public void notification(AppNotifications notifications) {
logger.info("ApplePayController.notification>>>>>>>>>>>>>>>>>>{notification}退款通知处理:notifications=",notifications);
iosPayService.notification(notifications);
}
}
需要注意 退款通知参数需要自己在项目定义 大家也可以自己参照ios开发文档自己编写(代码如下)
AppNotifications 类
import lombok.Data;
@Data
public class AppNotifications {
/**
* App Store Connect 生成的标识符,App Store 使用该标识符来唯一标识用户订阅续订的自动续订订阅。将此值视为 64 位整数
*/
private String auto_renew_adam_id;
/**
* 用户订阅续订的自动续订订阅的产品标识符。
*/
private String auto_renew_product_id;
/**
* 自动续订订阅产品的当前续订状态。请注意,这些值与收据中的值不同。auto_renew_status
* 可能的值: true, false
*/
private String auto_renew_status;
/**
* 用户打开或关闭自动续订订阅的续订状态的时间,采用类似于 ISO 8601 标准的日期时间格式
*/
private String auto_renew_status_change_date;
/**
* 用户打开或关闭自动续订订阅的续订状态的时间,采用 UNIX 纪元时间格式,以毫秒为单位。使用此时间格式来处理日期
*/
private String auto_renew_status_change_date_ms;
/**
* 用户打开或关闭自动续订订阅的续订状态的时间,以太平洋标准时间表示
*/
private String auto_renew_status_change_date_pst;
/**
* 包含应用程序包 ID 的字符串
*/
private String bid;
/**
* 包含应用程序包版本的字符串
*/
private String bvrs;
/**
* App Store 生成收据的环境。
* 可能的值: Sandbox, PROD
*/
private String environment;
/**
* 订阅过期的原因。此字段仅适用于过期的自动续订订阅
*/
private String expiration_intent;
/**
* 触发通知的订阅事件
*/
private String notification_type;
/**
* App Store 服务器通知为其发送的原始交易标识符。此字段仅出现时是CONSUMPTION_REQUESTnotification_typeCONSUMPTION_REQUEST
*/
private long original_transaction_id;
/**
* 与您在验证收据时在password字段中提交的共享机密相同的值。requestBody
*/
private String password;
/**
* 一个对象,其中包含有关应用程序最近的应用程序内购买交易的信息
*/
private Unified_receipt unified_receipt;
}
Unified_receipt类
import lombok.Data;
import java.util.List;
@Data
public class Unified_receipt {
/**
* App Store 生成收据的环境。
* 可能的值: Sandbox, Production
*/
private String environment;
/**
* 最新的 Base64 编码应用收据
*/
private byte latest_receipt;
/**
*包含解码值的最新 100 笔应用内购买交易的数组。此数组不包括您的应用标记为已完成的消费品的交易。
* 此数组的内容与用于接收验证的verifyReceipt端点响应中的内容相同。
* latest_receiptresponseBody.Latest_receipt_info
*/
private List latest_receipt_info;
/**
* 一个数组,其中每个元素都包含 中标识的每个自动续订订阅的挂起续订信息。
* 此数组的内容与用于接收验证的verifyReceipt端点响应中的内容相同。
* product_idresponseBody.Pending_renewal_info
*/
private List pending_renewal_info;
}
Latest_Receipt_Info 类
@Data
public class Latest_Receipt_Info {
/**
* 在与此交易相关联。仅当您的应用程序在用户购买时提供了该字段时,才会出现此字段;它只存在于沙盒环境中。appAccountTokenappAccountToken(_:)
*/
private String app_account_token;
/**
* App Store 以类似于 ISO 8601 的日期时间格式退款或撤销交易的时间。此字段仅适用于退款或撤销的交易
*/
private String cancellation_date;
/**
* App Store 退还交易或从家庭共享中撤销交易的时间,以 UNIX 纪元时间格式,以毫秒为单位。此字段仅适用于已退款或已撤销的交易。
* 使用此时间格式处理日期。有关更多信息,请参阅 。cancellation_date_ms
*/
private String cancellation_date_ms;
/**
* App Store 退款或取消家庭共享的时间,以太平洋标准时间为准。此字段仅适用于已退款或已撤销的交易
*/
private String cancellation_date_pst;
/**
* 退款或撤销交易的原因。值“1” 表示客户由于您的应用程序中的实际或感知问题取消了他们的交易。值“0” 表示交易因其他原因被取消;例如,如果客户不小心进行了购买。
* 可能的值: 1, 0
*/
private String cancellation_reason;
/**
* 订阅到期或续订的时间,以 UNIX 纪元时间格式,以毫秒为单位。使用此时间格式处理日期。请注意,此字段在收据中调用。expires_date_ms
*/
private String expires_date;
/**
* 订阅到期或续订的时间,以 UNIX 纪元时间格式,以毫秒为单位。使用此时间格式处理日期。有关更多信息,请参阅。expires_date_ms
*/
private String expires_date_ms;
/**
* 订阅到期或续订的时间,以太平洋标准时间表
*/
private String expires_date_pst;
/**
* 一个值,指示用户是产品的购买者,还是可以通过家庭共享访问产品的家庭成员。有关更多信息,请参阅。in_app_ownership_type
* 可能的值: FAMILY_SHARED, PURCHASED
*/
private String in_app_ownership_type;
/**
* 自动续订订阅是否处于介绍价格期的指标。有关更多信息,请参阅。is_in_intro_offer_period
* 可能的值: true, false
*/
private String is_in_intro_offer_period;
/**
* 订阅是否处于免费试用期的指标。有关更多信息,请参阅。is_trial_period
* 可能的值: true, false
*/
private String is_trial_period;
/**
* 由于用户升级,系统取消订阅的指示符。此字段仅适用于升级事务。
* 价值: true
*/
private String is_upgraded;
/**
* 您在 App Store Connect 中配置的订阅优惠的参考名称。当客户兑换订阅优惠代码时,会出现此字段。有关更多信息,请参阅。offer_code_ref_name
*/
private String offer_code_ref_name;
/**
* 原始应用购买的时间,采用类似于 ISO 8601 标准的日期时间格式
*/
private String original_purchase_date;
/**
* 原始应用购买的时间,以 UNIX 纪元时间格式,以毫秒为单位。使用此时间格式处理日期。此值表示订阅的初始购买日期。
* 原始购买日期适用于所有产品类型,并在同一产品 ID 的所有交易中保持不变。该值对应于 StoreKit 中原始事务的属性。transactionDate
*/
private String original_purchase_date_ms;
/**
* 原始应用购买的时间,采用太平洋标准时间
*/
private String original_purchase_date_pst;
/**
* 原始购买的交易标识符。有关更多信息,请参阅。original_transaction_id
*/
private String original_transaction_id;
/**
* 用户兑换的订阅优惠的标识符。有关更多信息,请参阅。promotional_offer_id
*/
private String promotional_offer_id;
/**
* 所购买产品的唯一标识符。您在 App Store Connect 中创建产品时提供此值,它对应于存储在事务属性中的对象的属性。productIdentifierSKPaymentpayment
*/
private String product_id;
/**
* App Store 以类似于 ISO 8601 标准的日期时间格式向用户帐户收取订阅购买或续订费用的时间
*/
private String purchase_date;
/**
* App Store 向用户帐户收取订阅购买或过期后续费的时间,采用 UNIX 纪元时间格式,以毫秒为单位。使用此时间格式处理日期
*/
private String purchase_date_ms;
/**
* App Store 向用户帐户收取订阅购买或过期后续订费用的时间,以太平洋标准时间计算
*/
private String purchase_date_pst;
/**
* 购买的消耗品数量。该值对应于存储在事务quantity属性中的SKPayment对象的payment属性。“1”除非使用可变付款修改,否则该值通常为。最大值为“10”
*/
private String quantity;
/**
* 订阅所属的订阅组标识。此字段的值与 中的属性相同。subscriptionGroupIdentifierSKProduct
*/
private String subscription_group_identifier;
/**
* 交易的唯一标识符,例如购买、恢复或续订。有关更多信息,请参阅。transaction_id
*/
private String transaction_id;
/**
* 跨设备购买事件的唯一标识符,包括订阅续订事件。该值是识别订阅购买的主键
*/
private String web_order_line_item_id;
}
Pending_Renewal_Info 类
import lombok.Data;
@Data
public class Pending_Renewal_Info {
/**
* 自动续订订阅的当前续订首选项。此键的值对应于客户订阅续订的产品的属性。productIdentifier
*/
private String auto_renew_product_id;
/**
* 自动续订订阅的当前续订状态。有关更多信息,请参阅。auto_renew_status
* 可能的值: 1, 0
*/
private String auto_renew_status;
/**
* 订阅过期的原因。此字段仅适用于包含过期、自动更新订阅的收据。有关更多信息,请参阅。expiration_intent
* 可能的值: 1, 2, 3, 4, 5
*/
private String expiration_intent;
/**
* 订阅续订宽限期到期的时间,采用类似于 ISO 8601 的日期时间格式
*/
private String grace_period_expires_date;
/**
* 订阅续订宽限期到期的时间,采用 UNIX 纪元时间格式,以毫秒为单位。此密钥仅适用于启用了计费宽限期的应用程序以及用户在续订时遇到计费错误时。使用此时间格式处理日期
*/
private String grace_period_expires_date_ms;
/**
* 订阅续订宽限期到期的时间,在太平洋时区
*/
private String grace_period_expires_date_pst;
/**
* 指示 Apple 正在尝试自动续订过期订阅的标志。此字段仅在自动续订订阅处于计费重试状态时出现。有关更多信息,请参阅。is_in_billing_retry_period
* 可能的值: 1, 0
*/
private String is_in_billing_retry_period;
/**
* 您在 App Store Connect 中配置的订阅优惠的参考名称。当客户兑换订阅优惠代码时,会出现此字段。有关更多信息,请参阅。offer_code_ref_name
*/
private String offer_code_ref_name;
/**
* 原始购买的交易标识符
*/
private String original_transaction_id;
/**
* 订阅价格上涨的价格同意状态。仅当 App Store 通知客户价格上涨时,才会出现此字段。如果客户同意,默认值为"0"和 更改为"1" 。
* 可能的值: 1, 0
*/
private String price_consent_status;
/**
* 所购买产品的唯一标识符。您在 App Store Connect 中创建产品时提供此值,它对应于存储在事务属性中的对象的属性。productIdentifierSKPaymentpayment
*/
private String product_id;
/**
* 用户兑换的自动续订订阅的促销优惠的标识符。在 App Store Connect 中创建促销优惠时,您在促销优惠标识符字段中提供此值。有关更多信息,请参阅。promotional_offer_id
*/
private String promotional_offer_id;
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)