参考别人的App Store Connect配置信息:https://www.pianshen.com/article/7151368564/

苹果抽成表一览(需登录):https://appstoreconnect.apple.com/apps/pricingmatrix

快速导航:

  1. 认证协议(签订银行信息)
  2. 设定商品价格
  3. 上线配置
  4. 注册沙箱环境
  5. java编码 tohashmap

准备工作 认证协议(签订银行信息)

首先进入App Store Connect 点击 协议、税务和银行业务

填写基本的银行信息。然后再填写一些基本信息(强制要求的填写,没强制的没必要!!!)

提交等待24小时,成功,回到App信息就能看到下面内容了

设定商品价格

App Connect 进入自己的APP,然后点击管理 (必须第一步的“认证协议”) 就能看到添加App 内购项目

注意:这里需要根据自己情况选择内购项目的类型

下图是你设置文本演示的对应支付时的展示位置

商品设定完成!前端可能需要这个产品ID,建议设置为 com.Company.Projece.Item 如:com.hefeixunliao.zhenliao.12yuan

上线设置

上述操作提交完成后,切记检查App内购项目的状态:元数据丢失的 内购项目可以进行沙箱测试,但上线不可用,上线必须是 准备提交状态

苹果说明

https://help.apple.com/app-store-connect/#/dev1986a0e5c

真实配置

上线务必保障再次填写下方内容!!!

配置完成后,你下次就拥有了 App内购项目了!

注册自己的沙箱账号:

添加沙箱账号

完成即可登录了。在APP内部测试的时候,会提示你是沙箱环境,建议使用沙箱账户!!!

如果需要更换自己的测试沙箱账户,请在Iphone – 设置 – App Store – 沙盒账户 点击可以退出登录。

Java编码

/**
 * @author : zanglikun
 * @date : 2021/11/18 9:40
 * @Version: 1.0
 * @Desc : 苹果支付 参考地址:https://www.cnblogs.com/shoshana-kong/p/10991753.html
 * sendHttpsCoon 方法里面包含了增加的业务逻辑
 */
@Slf4j
@Controller
@RequestMapping("applePay")
public class ApplePay {

    //购买凭证验证地址
    private static final String certificateUrl = "https://buy.itunes.apple.com/verifyReceipt";

    //测试的购买凭证验证地址
    private static final String certificateUrlTest = "https://sandbox.itunes.apple.com/verifyReceipt";

    /**
     * 重写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 {
        }
    };

    /**
     * 接收自己APP客户端发过来的购买凭证
     *
     * @param userId    用户ID
     * @param receipt   苹果传递前端给的值
     * @param chooseEnv 是否时测试环境
     */
    @PostMapping("/setIapCertificate")
    @ResponseBody
    @Transactional
    public String setIapCertificate(@ApiParam(value = "用户ID") String userId, @ApiParam(value = "苹果传递前端支付成功的值") String receipt, @ApiParam(value = "是否是真实环境,布尔值") boolean chooseEnv) {
        //log.info("IOS端发送的购买凭证。数据有 userId = {},receipt = {},chooseEnv = {}",userId,receipt,chooseEnv);
        if (StringUtils.isEmpty(userId) || StringUtils.isEmpty(receipt)) {
            return "用户ID 或者 receipt为空";
        }
        String url = null;
        url = chooseEnv == true ? certificateUrl : certificateUrlTest;
        final String certificateCode = receipt;
        if (StringUtils.isNotEmpty(certificateCode)) {
            String s = sendHttpsCoon(url, certificateCode, userId);
            if ("支付成功".equals(s)) {
                return s;
            } else {
                return s;
            }
        } else {
            return "receipt 为空!";
        }
    }

    /**
     * 发送请求 向苹果发起验证支付请求是否有效:本方法有认证方法进行调用
     *
     * @param url  支付的环境校验
     * @param code 接口传递的 receipt
     * @return 结果
     */
    private String sendHttpsCoon(String url, String code, String userId) {
        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);
            }
            System.out.println(sb);
            // 错误的 sb对象是:{"status":21002},苹果官网写的错误也都是2XXXX 具体含义可查:https://developer.apple.com/documentation/appstorereceipts/status
            // 所以 通过长度等于16,我们就可确定苹果支付成功是否有效
            if (sb.length() == 16) {
                return "支付失败,苹果说status异常";
            }
            // 将一个Json对象转成一个HashMap 
            JSONObject alljsoncode = JSON.parseObject(sb.toString());
            Object receipt = alljsoncode.get("receipt");
            HashMap hashMap = JSONUtil.toBean(receipt.toString(), HashMap.class);
            // 苹果给的订单号
            String original_transaction_id = (String) hashMap.get("original_transaction_id");
            //TODO 存储订单ID,并检查此订单ID是否存在,如果存在就证明已经发货了(避免二次发货)
            if ("com.hefeixunliao.zhenliao12yuan".equals(hashMap.get("product_id"))) {
                //TODO 执行12元的钻石增加
                log.info("用户ID:{},执行12元钻石追加",userId);
            } else if ("com.hefeixunliao.zhenliao30yuan".equals(hashMap.get("product_id"))) {
                //TODO 执行30元的钻石增加
                log.info("用户ID:{},执行30元钻石追加",userId);
            } else {
                log.info("用户ID:{},向苹果发起验证支付请求没有次product_id",userId);
                return "支付失败,当前没有次product_id";
            }
            return "支付成功";
        } catch (Exception e) {
            log.error("向苹果发起验证支付请求是否有效出现异常:{}", e.getMessage());
            return "支付过程中,出现了异常!";
        }
    }


    /**
     * 注意:下面代码跟苹果支付业务无关。
     * 这里的code 是前端请求苹果,苹果给前端的一个密钥(如果我们通过base64解密后,可获得signature、purchase-info、environment、pod、signing-status)这个密钥用于告诉Java服务器 想苹果服务器校验订单是否成功的参数
     */
    @Test
    public void jiemi() {
        String code = "ewoJInNpZ25hdHVyZSIgPSAiQXhmVWRiYUx5T2I5bllOM3hINmQzMnBaOHI2THdmV3ZmZ1NKN1o2QTM4dEY2SjNyUTZoRVZqQ3Rra01wMnhmM1pwWnFQRmw3ZlRIdDVxNVpKZUF6UWh4NWQ1djJrR01uM3NKb3ZBWXNuWENxY3VqclBWU3A5WTFYUTZjeTlvbVNORWNYVWt0L1dkQXhsRmN6WDRZMTJzcktsMDc3WHJIdk5JMDd0VTZXajgzbVdDNE1HZmF0c2E2UEo1RG5sT2lEOG96RlJ6a0NIQ3Y3bncvRm80dnFCaFpZRmlQSDZzeW1uN2lUQlhTcXlTdlJOTGJXLytUWktKZngxR1dRV3BWdmJ5M0RtV3l4OTRaYkxGRllNODE0aTB2a1lnWDdPdVUwQWprTVFKOEJhNnJGc1hER0hYY2FCdnVhZ1NGak9iMWdJclZ2MDdmbTlBV3ltNE5KT0dVSHR0Z0FBQVdBTUlJRmZEQ0NCR1NnQXdJQkFnSUlEdXRYaCtlZUNZMHdEUVlKS29aSWh2Y05BUUVGQlFBd2daWXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Td3dLZ1lEVlFRTERDTkJjSEJzWlNCWGIzSnNaSGRwWkdVZ1JHVjJaV3h2Y0dWeUlGSmxiR0YwYVc5dWN6RkVNRUlHQTFVRUF3dzdRWEJ3YkdVZ1YyOXliR1IzYVdSbElFUmxkbVZzYjNCbGNpQlNaV3hoZEdsdmJuTWdRMlZ5ZEdsbWFXTmhkR2x2YmlCQmRYUm9iM0pwZEhrd0hoY05NVFV4TVRFek1ESXhOVEE1V2hjTk1qTXdNakEzTWpFME9EUTNXakNCaVRFM01EVUdBMVVFQXd3dVRXRmpJRUZ3Y0NCVGRHOXlaU0JoYm1RZ2FWUjFibVZ6SUZOMGIzSmxJRkpsWTJWcGNIUWdVMmxuYm1sdVp6RXNNQ29HQTFVRUN3d2pRWEJ3YkdVZ1YyOXliR1IzYVdSbElFUmxkbVZzYjNCbGNpQlNaV3hoZEdsdmJuTXhFekFSQmdOVkJBb01Da0Z3Y0d4bElFbHVZeTR4Q3pBSkJnTlZCQVlUQWxWVE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBcGMrQi9TV2lnVnZXaCswajJqTWNqdUlqd0tYRUpzczl4cC9zU2cxVmh2K2tBdGVYeWpsVWJYMS9zbFFZbmNRc1VuR09aSHVDem9tNlNkWUk1YlNJY2M4L1cwWXV4c1FkdUFPcFdLSUVQaUY0MWR1MzBJNFNqWU5NV3lwb041UEM4cjBleE5LaERFcFlVcXNTNCszZEg1Z1ZrRFV0d3N3U3lvMUlnZmRZZUZScjZJd3hOaDlLQmd4SFZQTTNrTGl5a29sOVg2U0ZTdUhBbk9DNnBMdUNsMlAwSzVQQi9UNXZ5c0gxUEttUFVockFKUXAyRHQ3K21mNy93bXYxVzE2c2MxRkpDRmFKekVPUXpJNkJBdENnbDdaY3NhRnBhWWVRRUdnbUpqbTRIUkJ6c0FwZHhYUFEzM1k3MkMzWmlCN2o3QWZQNG83UTAvb21WWUh2NGdOSkl3SURBUUFCbzRJQjF6Q0NBZE13UHdZSUt3WUJCUVVIQVFFRU16QXhNQzhHQ0NzR0FRVUZCekFCaGlOb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxYZDNaSEl3TkRBZEJnTlZIUTRFRmdRVWthU2MvTVIydDUrZ2l2Uk45WTgyWGUwckJJVXdEQVlEVlIwVEFRSC9CQUl3QURBZkJnTlZIU01FR0RBV2dCU0lKeGNKcWJZWVlJdnM2N3IyUjFuRlVsU2p0ekNDQVI0R0ExVWRJQVNDQVJVd2dnRVJNSUlCRFFZS0tvWklodmRqWkFVR0FUQ0IvakNCd3dZSUt3WUJCUVVIQWdJd2diWU1nYk5TWld4cFlXNWpaU0J2YmlCMGFHbHpJR05sY25ScFptbGpZWFJsSUdKNUlHRnVlU0J3WVhKMGVTQmhjM04xYldWeklHRmpZMlZ3ZEdGdVkyVWdiMllnZEdobElIUm9aVzRnWVhCd2JHbGpZV0pzWlNCemRHRnVaR0Z5WkNCMFpYSnRjeUJoYm1RZ1kyOXVaR2wwYVc5dWN5QnZaaUIxYzJVc0lHTmxjblJwWm1sallYUmxJSEJ2YkdsamVTQmhibVFnWTJWeWRHbG1hV05oZEdsdmJpQndjbUZqZEdsalpTQnpkR0YwWlcxbGJuUnpMakEyQmdnckJnRUZCUWNDQVJZcWFIUjBjRG92TDNkM2R5NWhjSEJzWlM1amIyMHZZMlZ5ZEdsbWFXTmhkR1ZoZFhSb2IzSnBkSGt2TUE0R0ExVWREd0VCL3dRRUF3SUhnREFRQmdvcWhraUc5Mk5rQmdzQkJBSUZBREFOQmdrcWhraUc5dzBCQVFVRkFBT0NBUUVBRGFZYjB5NDk0MXNyQjI1Q2xtelQ2SXhETUlKZjRGelJqYjY5RDcwYS9DV1MyNHlGdzRCWjMrUGkxeTRGRkt3TjI3YTQvdncxTG56THJSZHJqbjhmNUhlNXNXZVZ0Qk5lcGhtR2R2aGFJSlhuWTR3UGMvem83Y1lmcnBuNFpVaGNvT0FvT3NBUU55MjVvQVE1SDNPNXlBWDk4dDUvR2lvcWJpc0IvS0FnWE5ucmZTZW1NL2oxbU9DK1JOdXhUR2Y4YmdwUHllSUdxTktYODZlT2ExR2lXb1IxWmRFV0JHTGp3Vi8xQ0tuUGFObVNBTW5CakxQNGpRQmt1bGhnd0h5dmozWEthYmxiS3RZZGFHNllRdlZNcHpjWm04dzdISG9aUS9PamJiOUlZQVlNTnBJcjdONFl0UkhhTFNQUWp2eWdhWndYRzU2QWV6bEhSVEJoTDhjVHFBPT0iOwoJInB1cmNoYXNlLWluZm8iID0gImV3b0pJbTl5YVdkcGJtRnNMWEIxY21Ob1lYTmxMV1JoZEdVdGNITjBJaUE5SUNJeU1ESXhMVEV4TFRJMklESXlPalU1T2pJd0lFRnRaWEpwWTJFdlRHOXpYMEZ1WjJWc1pYTWlPd29KSW5WdWFYRjFaUzFwWkdWdWRHbG1hV1Z5SWlBOUlDSTROelU1WmpNMVlUTTNNMk0wTVRabU5qazRPVFJrWkRRd05XRTFOemhoTURoalpqSTVOMlkwSWpzS0NTSnZjbWxuYVc1aGJDMTBjbUZ1YzJGamRHbHZiaTFwWkNJZ1BTQWlNVEF3TURBd01Ea3hPVE13TWpFeU5DSTdDZ2tpWW5aeWN5SWdQU0FpTVRBd0lqc0tDU0owY21GdWMyRmpkR2x2YmkxcFpDSWdQU0FpTVRBd01EQXdNRGt4T1RNd01qRXlOQ0k3Q2draWNYVmhiblJwZEhraUlEMGdJakVpT3dvSkltbHVMV0Z3Y0MxdmQyNWxjbk5vYVhBdGRIbHdaU0lnUFNBaVVGVlNRMGhCVTBWRUlqc0tDU0p2Y21sbmFXNWhiQzF3ZFhKamFHRnpaUzFrWVhSbExXMXpJaUE5SUNJeE5qTTNPVGsyTXpZd01qYzNJanNLQ1NKMWJtbHhkV1V0ZG1WdVpHOXlMV2xrWlc1MGFXWnBaWElpSUQwZ0lqaEJSRVJHUlRjMUxURkVRVVl0TkRVek1TMDRNakV3TFVKRE9UZzNSa1l3TlRrNU9DSTdDZ2tpY0hKdlpIVmpkQzFwWkNJZ1BTQWlZMjl0TG1obFptVnBlSFZ1YkdsaGJ5NTZhR1Z1YkdsaGJ6TXdlWFZoYmlJN0Nna2lhWFJsYlMxcFpDSWdQU0FpTVRVNU5qWXpNRGcyT0NJN0Nna2lZbWxrSWlBOUlDSmpiMjB1YUdWbVpXbDRkVzVzYVdGdkxucG9aVzVzYVdGdklqc0tDU0pwY3kxcGJpMXBiblJ5YnkxdlptWmxjaTF3WlhKcGIyUWlJRDBnSW1aaGJITmxJanNLQ1NKd2RYSmphR0Z6WlMxa1lYUmxMVzF6SWlBOUlDSXhOak0zT1RrMk16WXdNamMzSWpzS0NTSndkWEpqYUdGelpTMWtZWFJsSWlBOUlDSXlNREl4TFRFeExUSTNJREEyT2pVNU9qSXdJRVYwWXk5SFRWUWlPd29KSW1sekxYUnlhV0ZzTFhCbGNtbHZaQ0lnUFNBaVptRnNjMlVpT3dvSkluQjFjbU5vWVhObExXUmhkR1V0Y0hOMElpQTlJQ0l5TURJeExURXhMVEkySURJeU9qVTVPakl3SUVGdFpYSnBZMkV2VEc5elgwRnVaMlZzWlhNaU93b0pJbTl5YVdkcGJtRnNMWEIxY21Ob1lYTmxMV1JoZEdVaUlEMGdJakl3TWpFdE1URXRNamNnTURZNk5UazZNakFnUlhSakwwZE5WQ0k3Q24wPSI7CgkiZW52aXJvbm1lbnQiID0gIlNhbmRib3giOwoJInBvZCIgPSAiMTAwIjsKCSJzaWduaW5nLXN0YXR1cyIgPSAiMCI7Cn0=";
        byte[] decode = Base64.decode(code.getBytes());
        System.out.println(new String(decode));
    }

}

先看下每个字段的含义,在看输出的结果

字段类型描述
transaction_idinteger交易号
original_transaction_idinteger原始交易号
product_idstring商品标识符
quantityinteger数量
purchase_datestring购买日期
original_purchase_datestring原始购买日期
purchase_date_msinteger购买日期(ms)
original_purchase_date_msinteger原始购买日期(ms)
purchase_date_pststring购买日期(pst)
original_purchase_date_pststring原始购买日期(pst)
cancellation_datestring取消购买的日期

返回的结果是(我已经Json格式化了):

{
    "data":{
        "receipt":{
            "original_purchase_date_pst":"2021-11-26 18:24:16 America/Los_Angeles",
            "purchase_date_ms":"1637979856000",
            "unique_identifier":"8759f35a373c416f69894dd405a578a08cf297f4",
            "original_transaction_id":"1000000919276978",
            "bvrs":"100",
            "transaction_id":"1000000919276978",
            "quantity":"1",
            "in_app_ownership_type":"PURCHASED",
            "unique_vendor_identifier":"8ADDFE75-1DAF-4531-8210-BC987FF05998",
            "item_id":"1596630862",
            "original_purchase_date":"2021-11-27 02:24:16 Etc/GMT",
            "is_in_intro_offer_period":"false",
            "product_id":"com.hefeixunliao.zhenliao12yuan",
            "purchase_date":"2021-11-27 02:24:16 Etc/GMT",
            "is_trial_period":"false",
            "purchase_date_pst":"2021-11-26 18:24:16 America/Los_Angeles",
            "bid":"com.hefeixunliao.zhenliao",
            "original_purchase_date_ms":"1637979856000"
        },
        "status":0
    },
    "statusCode":200,
    "header":{
        "Date":"Sat, 27 Nov 2021 02:25:22 GMT",
        "Keep-Alive":"timeout=60",
        "Content-Length":"794",
        "Connection":"keep-alive",
        "Content-Type":"text/plain;charset=UTF-8",
        "Vary":"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
    },
    "errMsg":"request:ok",
    "cookies":[

    ]
}

代码编写完成,去重新打包吧。搞完切记回头看下 #5