本文共 10396 字,大约阅读时间需要 34 分钟。
转自胖哥的整理,地址:
苹果说明文档:
这是一篇文摘性文章。
响应速度比方法二快。
public static JSONObject verifyReceipt1(String recepit) { return verifyReceipt1("https://buy.itunes.apple.com/verifyReceipt", recepit); } public static JSONObject verifyReceipt1(String url, String receipt) { try { HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setAllowUserInteraction(false); PrintStream ps = new PrintStream(connection.getOutputStream()); ps.print("{\"receipt-data\": \"" + receipt + "\"}"); ps.close(); BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream())); String str; StringBuffer sb = new StringBuffer(); while ((str = br.readLine()) != null) { sb.append(str); } br.close(); String resultStr = sb.toString(); JSONObject result = JSONObject.parseObject(resultStr); if (result != null && result.getInteger("status") == 21007) { return verifyReceipt1("https://sandbox.itunes.apple.com/verifyReceipt", receipt); } return result; } catch (Exception e) { e.printStackTrace(); } return null; }
public static JSONObject verifyReceipt2(String receipt) { return verifyReceipt2("https://buy.itunes.apple.com/verifyReceipt", receipt); } public static JSONObject verifyReceipt2(String url, String receipt) { HttpClient httpClient = new DefaultHttpClient(); HttpPost httpPost = new HttpPost(url); try { JSONObject data = new JSONObject(); data.put("receipt-data", receipt); StringEntity entity = new StringEntity(data.toJSONString()); entity.setContentEncoding("utf-8"); entity.setContentType("application/json"); httpPost.setEntity(entity); HttpResponse response = httpClient.execute(httpPost); HttpEntity httpEntity = response.getEntity(); String resultStr = EntityUtils.toString(httpEntity); JSONObject result = JSONObject.parseObject(resultStr); httpPost.releaseConnection(); if (result.getInteger("status") == 21007) { return verifyReceipt2("https://sandbox.itunes.apple.com/verifyReceipt", receipt); } return result; } catch (Exception e) { e.printStackTrace(); } return null; }
这里的代码仅仅是从苹果获取了JSON对象,并未进行响应的验证。
我们来细细看一下返回的JSON,大概是下边这个样子的:
{ "status": 0, "environment": "Production", "receipt": { "receipt_type": "Production", "adam_id": 2341443613, "app_item_id": 2234443613, "bundle_id": "com.xxxxx.xxxxx", "application_version": "1", "download_id": 23456572706673, "version_external_ident ifier": 821223402, "receipt_creation_date": "2017-01-25 00:52:37 Etc/GMT", "receipt_creation_date_ms": "3333897657000", "receipt_creation_date_pst": "2017-01-25 17:57:37 America/Los_Angeles", "request_date": "2017-01-26 00:57:38 Etc/GMT", "request_date_ms": "1445897657000", "request_date_pst": "2017-05-29 17:57:38 America/Los_Angeles", "original_purchase_date": "2016-01-25 15:37:18 Etc/GMT", "original_purchase_ date_ms": "145234568000", "original_purchase_date_pst": "2016-01-25 07:37:18 America/Los_Angeles", "original_application_version": "12", "in_app": [ { "quantity": "1", "product_id": "xxxxxxxxx", "transaction_id": "110000290198443", "original_transaction_id": "110000290198443", "purchase_date": "2017-01-26 00:23:36 Etc/GMT", "purchase_date_ms": "1496105856000", "purchase_date_pst": "2017-01-26 00:35:30 America/Los_Angeles", "original_purchase_date": "2017-01-26 00:57:36 Etc/GMT", "original_purchase_date_ms": "14347896000", "original_purchase_date_pst": "2017-01-25 17:57:36 America/Los_Angeles", "is_trial_period": "false" } ] }}
—————————————————————————————————————————————————————————
重点解释一下in_app,此处是导致漏单情况最严重的地方
in_app返回的是JsonArrary,就是如果该用户支付多次,前两次没有校验(没走完Apple Pay的完整流程链),则最近一次校验会把前几次的校验串返回。in_app返回的是空,也说明校验是有效的。
这样就需要处理JsonArrary的数量>1的情况,取最新的交易时间的,而且价格匹配上,进行去充值操作,额外数据的进行数据存储,方便后续用户投诉时,进行查找凭证。
如果用户过来的请求也可以去匹配一下额外数据存储表,如果匹配成功,则可以充值。(谨慎使用,如果客服有权限补单充值的)
transaction_id:最新票据交易号
original_transaction_id:最初的票据交易号
网上也有相同的情况,参数地址:https://www.cnblogs.com/widgetbox/p/8241333.html
看完这个就很好理解上面出现的问题了,也就是说:
验证票据返回的receipt里面的in_app字段,这个字段包含了所有你未完成交易的票据信息。也就是在上面说到的APP完成交易之后,这个票据信息,就会从in_app中消失。
如果APP不完成交易,这个票据信息就会在in_app中一直保留。(这个情况可能仅限于你的商品类型为消耗型)
知道了事件的原委,就很好优化解决了,方案有2个
1.对票据返回的in_app数据全部进行处理,没有充值的全部进行充值
2.仅对最新的充值信息进行处理(我们采取的方案)
因为采用二方案:
如果用户仅进行了一次充值,该充值未到账,他不再进行充值了,那么会无法导致。
如果他通过客服的途径已经进行了补充充值,那么他在下一次充值的时候依旧会把之前的产品票据带回,这时候有可能出现重复充值的情况
—————————————————————————————————————————————————————————
解读一下status:
0 正常21000 App Store不能读取你提供的JSON对象21002 receipt-data域的数据有问题21003 receipt无法通过验证21004 提供的shared secret不匹配你账号中的shared secret21005 receipt服务器当前不可用21006 receipt合法,但是订阅已过期。服务器接收到这个状态码时,receipt数据仍然会解码并一起发送21007 receipt是Sandbox receipt,但却发送至生产系统的验证服务21008 receipt是生产receipt,但却发送至Sandbox环境的验证服务
不难发现我们可以利用 in_app中的quantity、product_id、transaction_id、purchase_date来对支付内容进行检查,当然了记录下返回的receipt文本串也是个不错的方法。
网上有人用MD5值的方法来防止重复支付,其实transaction_id也是可以做唯一区分的。以下是一部分来自网上的代码,
public class IOSAction extends BaseAction{ private static final long serialVersionUID = 1L; /** * 客户端向服务器验证 * * * * checkState A 验证成功有效(返回收据) * B 账单有效,但己经验证过 * C 服务器数据库中没有此账单(无效账单) * D 不处理 * * @return * @throws IOException */ public void IOSVerify() throws IOException { HttpServletRequest request=ServletActionContext.getRequest(); HttpServletResponse response=ServletActionContext.getResponse(); System.out.println(new Date().toLocaleString()+" 来自苹果端的验证..."); //苹果客户端传上来的收据,是最原据的收据 String receipt=request.getParameter("receipt"); System.out.println(receipt); //拿到收据的MD5 String md5_receipt=MD5.md5Digest(receipt); //默认是无效账单 String result=R.BuyState.STATE_C+"#"+md5_receipt; //查询数据库,看是否是己经验证过的账号 boolean isExists=DbServiceImpl_PNM.isExistsIOSReceipt(md5_receipt); String verifyResult=null; if(!isExists){ String verifyUrl=IOS_Verify.getVerifyURL(); verifyResult=IOS_Verify.buyAppVerify(receipt, verifyUrl); //System.out.println(verifyResult); if(verifyResult==null){ //苹果服务器没有返回验证结果 result=R.BuyState.STATE_D+"#"+md5_receipt; }else{ //跟苹果验证有返回结果------------------ JSONObject job = JSONObject.fromObject(verifyResult); String states=job.getString("status"); if(states.equals("0"))//验证成功 { String r_receipt=job.getString("receipt"); JSONObject returnJson = JSONObject.fromObject(r_receipt); //产品ID String product_id=returnJson.getString("product_id"); //数量 String quantity=returnJson.getString("quantity"); //跟苹果的服务器验证成功 result=R.BuyState.STATE_A+"#"+md5_receipt+"_"+product_id+"_"+quantity; //交易日期 String purchase_date=returnJson.getString("purchase_date"); //保存到数据库 DbServiceImpl_PNM.saveIOSReceipt(md5_receipt, product_id, purchase_date, r_receipt); }else{ //账单无效 result=R.BuyState.STATE_C+"#"+md5_receipt; } //跟苹果验证有返回结果------------------ } //传上来的收据有购买信息==end============= }else{ //账单有效,但己验证过 result=R.BuyState.STATE_B+"#"+md5_receipt; } //返回结果 try { System.out.println("验证结果 "+result); System.out.println(); response.getWriter().write(result); } catch (IOException e) { e.printStackTrace(); } } }
有些特殊场景,还是需要前端配合去做的。下边摘录的内容值得了解。
建立在IAP Server Model的基础上,并且我们知道手机网络是不稳定的,在付款成功后不能确保把receipt-data一定提交到服务器。如果出现了这样的情况,那就意味着玩家被appstore扣费了,却没收到服务器发放的道具。
解决这个问题的方法是在客户端提交receipt-data给我们的服务器,让我们的服务器向苹果服务器发送验证请求,验证这个receipt-data账单的有效性. 在没有收到回复之前,客户端必须要把receipt-data保存好,并且定期或在合理的UI界面触发向服务端发起请求,直至收到服务端的回复后删除客户端的receipt账单记录。这里就是我在开头提到的漏单处理了。 如果是客户端没成功提交receipt-data,那怎么办?就是玩家被扣费了,也收到appstore的消费收据了,却依然没收到游戏道具,于是投诉到游戏客服处。 这种情况在以往的经验中也会出现,常见的玩家和游戏运营商发生的纠纷。游戏客服向玩家索要游戏账号和appstore的收据单号,通过查询itunes-connect看是否确有这笔订单。如果订单存在,则要联系研发方去查询游戏服务器,看订单号与玩家名是否对应,并且是否已经被使用了,做这一点检查的目的是 为了防止恶意玩家利用已经使用过了的订单号进行欺骗(已验证的账单是可以再次请求验证的,曾经为了测试,将账单手动发给服务器处理并成功),谎称自己没收到商品。这就是上面一节IAP Server Model中红字所提到的安全逻辑的目的。当然了,如果查不到这个订单号,就意味着这个订单确实还没使用过,手动给玩家补发商品即可。 有朋友问怎么通过itunes-connect查看具体订单,itunes-connect中无法直接看到订单信息,可以用以下方法来查询