苹果钱包(AppleWallet)接入操作手册,超详细

news2024/11/30 12:44:06

一、写在前面

1.1 起源

最近有一点忙,更新博客出现了一些延迟。由于在工作中使用了Apple Wallet,所以在结束后准备以此记录一下。希望后面有要做此功能的同学,能够有所启发,觉得有帮助的,记得点赞收藏关注哦~

1.2 Apple Wallet 扫盲:

1.2.1 什么是Apple Wallet?

  • 总体来说:苹果钱包是Apple推出的软件,可以用于集中保管和使用的信用卡、借记卡、交通卡、登机牌、活动门票、学生证等等。全都收纳在您 iPhone 或 Apple Watch 的“钱包”中。
  • 简单来说:我们日常会办理非常多的卡片,比如银行卡、礼品卡、购物卡、会员卡等等,日常出行中这些卡片不易携带,但是又不可或缺。使用Apple Wallet能够将这些卡片装入Apple Wallet 这个软件中,更加方便快捷;

1.2.2 使用Apple Wallet 的优点

  • 从用户的角度:
    • 方便,使用apple wallet 卡片能够不用带实体卡
    • 使用简单,只需要扫描即可添加到设备中
    • 不易丢失,卡片能够通过icloud 同步,随时保存和分享
    • 新颖,与传统的实体卡更进一步
    • 管理方便,一个app就可以管理所有的卡片;
    • 能够查看更多的信息,卡片还能够随时更新和获取推送;
  • 从企业的角度:
    • 扩展业务,丰富使用场景,比竞品更多竞争力。
    • 增强用户体验,随时随地能够使用apple wallet 卡片来进行交易、认证、提醒等;
    • 独立使用,即便用户不下载企业应用、登录企业网站,依然可以在apple wallet中获取到企业生产的礼品卡,并随时获取最新内容;
    • 成本低,通过直接与Apple Wallet 集成,无缝衔接。Apple Wallet 的后期生成、更新不会收费。
    • 提高产品能力,比如机票卡片能够让用户及时收到飞机起飞前的通知提醒,会员卡可以让用户不需要带着实体卡就可以进店结账消费;

1.2.2 我们接入了Apple Wallet 能够做什么?

Apple Wallet 为我们提供了丰富的卡片场景,主要的卡片类型有以下几种:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

简单来说,提供的这几种通行证(Apple Wallet 卡片)区分的类型只是为我们描述了具体的使用场景。我们可以根据实际需要来选择合适的卡片。

1.2.3 什么是通行证?

  • 上面提到了,通行证就是Apple Wallet 的卡片。同时也是我们发给用户、用户使用的卡片的统称;它的具体形式可以是礼品卡、活动门票等;

1.2.4 如何选择合适的通行证?

  • 对于通行证来说,不同的通行证类型卡片上展示的字段存在差异,同时排列的位置也会有一些区别。所以我们只需要根据实际要展示的元素,选择合适的通行证即可;
  • 如果是要做某一个通行证,选择对应类型的通行证无法满足要求的话,我们可以选择通用通行证,它往往卡片的字段更多,能容易满足要求;

1.2.5 如何生成和使用通行证?

  • 如何生成通行证:
    • 注册成为Apple Developer
    • 申请Apple Wallet 安全证书(一般有效期为一年)
    • 设计礼品卡样式,可点击查看官方参考文档
    • 将安全证书放入你的项目工程中,并引用jpasskit 工程创建礼品卡文件;
    • 将此文件传给调用方即可使用。这个接口我们可以使用 Add to Apple Wallet 图标来做触发,可点击查看官方参考文档
  • 如何传递通行证:
    • 在手机端上来说,用户点击了Add Apple Wallet图标后触发调用后端接口,拿到了 xxx.pkpass 的通行证,使用Safari 浏览器会自动识别,并在用户允许后添加至用户苹果设备的Apple Wallet中;
    • 在Web端上来说,我们可以使用二维码来做展示;当用户点击了Add Apple Wallet图标后会显示一个二维码,用户通过使用原生相机扫描后可以跳转至Safari 浏览器触发后端接口,然后拿到通行证后添加至Apple Wallet;
  • 如何添加通行证:
    • 对于用户来说,拿到通行证后会出现如下的预览界面,用户点击添加之后即可加入到自己的Apple Wallet 中:
      • 在这里插入图片描述
    • 对于开发人员来说,需要使用jpasskit 来生成通行证,然后再发送给页面即可;后面会详细介绍这部分内容;
  • 如何使用通行证:
    • 主要看通行证对于企业来说,用于什么目的,做对应的查看、使用、提醒等等即可;
    • 比如登记牌卡片,对于航空公司来说可以设置地理位置,当客户到达机场能够自动识别并对应处理,也可以在飞机起飞前40分钟对用户提醒,提醒用户及时登机等;(礼品卡支持地理位置、通知、甚至NFC功能)
  • 如何更新通行证
    • 对于用户来说,只需要下拉刷新即可更新。或者用户在企业系统中对应礼品卡数据发生了变更会自动推送更新;对于后者,比如用户使用礼品卡进行了消费,那么礼品卡的余额会发生扣减,服务器会同步信息到Apple Wallet 中;
    • 对于开发人员来说,我们关注的也只有手动更新和自动更新两种形式;一种是用户手动下拉刷新,一种是服务器推送至手机;实现细节后面会进行详细介绍;
  • 如何删除通行证:
    • 对于用户来说,只需要简单的在Apple Wallet删除对应通行证即可;

1.3 前置准备

  • Apple Developer Account: 对于前置准备,我们最需要的就是Apple 开发者账号,这个可以请公司对应人员帮忙注册;
  • 安全证书:注册账号之后,需要在Apple Developer 中生成安全证书,它用于与Apple Wallet 推送通知、生成通行证能够被Apple 设备信任的要素;
  • 证书形式如下,有效期一般为一年,需要定时替换最新:
    在这里插入图片描述

此步骤可以请公司内相关人员帮忙注册和生成,同时网上相关的资料应该很多,此文不加赘述;

二、创建通行证卡片

2.1 理论知识

  1. 通行证的创建从原理上来说很简单

    • 前端页面需要使用官方的Add to Apple Wallet图标来支持Apple Walllet功能
    • 后端提供一个接口,当用户点击Add to Apple Wallet图标后会调用这个接口来获取一个通行证文件;
    • 这个通行证文件是一个压缩文件,像这样:
      • 在这里插入图片描述
      • 我们将其pkpass 后缀修改为zip,可以将其解压,能够看到里面的结构;
    • 如何创建这样的一个通行证呢?
      1. 首先设计好通行证的样式和字段,设计好之后就知道了通行证的预期样式是什么样的了;
      2. 然后引入jpasskit 依赖到项目中;
      3. 再然后将安全证书导入到resource 目录中;
      4. 最后就可以使用jpasskit 的工具类PKFileBasedSigningUtil调用createSignedAndZippedPkPassArchive方法创建通行证字节码数组;
      5. 将字节码数组写入流,并发送给前端;
  2. 接下来我将用实际代码演示这个过程;

2.2 代码演示

  1. 设计通行证,点击查看官方参考文档
  2. 引入jpasskit 依赖到项目中:
<dependency>
    <groupId>de.brendamour</groupId>
    <artifactId>jpasskit</artifactId>
    <version>0.1.2</version>
</dependency>

如果出现找不到类等情况,就看一下是否缺失某些依赖,因为jpasskit里面还关联了其他的一些依赖,但是目前项目中没有的;

  1. 将证书导入项目中,如图所示:
    在这里插入图片描述
  2. 加载安全证书
    1. 使用本地真实路径加载证书(可以本地测试使用, 其中PROJECT_PATH可以换成真实的全路径):
      String privateKeyPassword = "xxxx"; // the password you used to exportprivateKeyPath
      String appleWWDRCA = PROJECT_PATH + "AppleWWDRCA.cer"; // this is apple's developer relation cert
      String privateKeyPath = PROJECT_PATH + "passtype_key_dev.p12"; // the private key you exported from keychain
      try {
          PKSigningInformation pkSigningInformation = PKSigningUtil.
                  loadSigningInformationFromPKCS12FileAndIntermediateCertificateFile(
                          privateKeyPath, privateKeyPassword, appleWWDRCA);
      } catch (Exception e){
      }
      
    2. 使用流的形式加载证书(可以用于服务器环境)
      	// 这里密码是你自己的证书密码
          public static final String PRIVATE_KEY_PASSWORD = "xxxx";
          public static final String APPLE_WWDRCA = "AppleWWDRCA.cer";
          public static final String PASSTYPE_KEY_DEV = "passtype_key_dev.p12";
      
          @PostConstruct
          public void init() {
              try {
                  appleWWDRCA = ClassUtils.getDefaultClassLoader().getResourceAsStream(APPLE_WWDRCA);
                  privateKeyPath = ClassUtils.getDefaultClassLoader().getResourceAsStream(PASSTYPE_KEY_DEV);
                  pkSigningInformation = new PKSigningInformationUtil()
                          .loadSigningInformationFromPKCS12AndIntermediateCertificate(privateKeyPath
                                  , PRIVATE_KEY_PASSWORD,
                                  appleWWDRCA);
              } catch (Exception e) {
              }
          }
      

如果是本地测试,直接用方法1,要发布到测试环境了可以用方法二来创建证书;

  1. 创建通行证卡片,以下为礼品卡的示例代码
 // 这里PKPass 是通行证对象,我们需要填充里面的内容;
 PKPass pass = new PKPass();
 // 固定设置1
 pass.setFormatVersion(1);
 // 这个字段用户apple wallet 更新,如果不需要Apple Wallet 更新可以去掉,目前只做创建也可以先删除调
 // pass.setWebServiceURL(new URL("https://xxxx:xxx/"));
 // 这个信息从Apple Developer中获取,对应替换成自己的
 pass.setPassTypeIdentifier("pass.com.xxx.xxx.xxx");
 // 这个信息从Apple Developer中获取, 对应替换成自己的
 // pass.setAuthenticationToken("xxxxxxxxxxxxxxxxx");
 String serialNumber = UUID.randomUUID().toString();
 // 这个是通行证唯一id,如果每次生成都是一个新的,那么可以每次重新New UUID。如果要去重,就需要保存这个ID,将它与业务id数据形成一对一关系;
 pass.setSerialNumber(serialNumber);
 // 来源于Apple Developer
 pass.setTeamIdentifier("xxxxxx"); // replace this with your team ID
 // 以下的信息都是通行证中的通用基本元素
 pass.setOrganizationName("Org name");
 pass.setBackgroundColor("#0F0242");
 pass.setForegroundColor("#FFFFFF");
 pass.setLabelColor("#FFFFFF");
 pass.setDescription("some description");
 pass.setLogoText("");
 pass.setExpirationDate(new Date());
 // 通行证中的二维码
 PKBarcode barcode = new PKBarcode();
 barcode.setFormat(PKBarcodeFormat.PKBarcodeFormatQR);
 barcode.setMessage("123456789");
 barcode.setMessageEncoding(Charset.forName("UTF-8"));
 pass.setBarcodes(Arrays.asList(barcode));
 // 这里的是通行证中的一些指定通行证的对应元素;如果不是使用PKStoreCard,替换成对应的就可以,然后根据自己预先设计的礼品卡样式来填充即可;
 PKStoreCard pkStoreCard = new PKStoreCard();        
 PKField pkField = new PKField();
 pkField.setKey("CardName");
 pkField.setValue("JD E-card");
 pkStoreCard.setPrimaryFields(ImmutableList.of(pkField));
 pass.setStoreCard(pkStoreCard);
 
 // 地理位置,可以不设置;
 PKLocation location = new PKLocation();
 location.setLatitude(xxxx); // replace with some lat
 location.setLongitude(xxxx); // replace with some long
 List<PKLocation> locations = new ArrayList<PKLocation>();
 locations.add(location);
 pass.setLocations(locations);
 
 // 这里 isValid可以判断前面的元素是否都满足了,如果通过了就继续填充logo、图标等图形;
 if (pass.isValid()) {
     PKPassTemplateInMemory pkPassTemplateInMemory = new PKPassTemplateInMemory();
     // 这里PROJECT_PATH 替换为自己的项目文件路径
     pkPassTemplateInMemory.addFile(PKPassTemplateInMemory.PK_ICON, PROJECT_PATH + "images" + FILE_SEPARATOR + "icon.png");
     pkPassTemplateInMemory.addFile(PKPassTemplateInMemory.PK_ICON_RETINA, PROJECT_PATH + "images" + FILE_SEPARATOR + "icon@2x.png");
     pkPassTemplateInMemory.addFile(PKPassTemplateInMemory.PK_ICON_RETINAHD, PROJECT_PATH + "images" + FILE_SEPARATOR + "icon@3x.png");
     PKFileBasedSigningUtil pkSigningUtil = new PKFileBasedSigningUtil();
     // 传入通行证内容、通行证图片模板、和通行证安全证书,生成通行证文件byte[] 数据;
     byte[] signedAndZippedPkPassArchive = pkSigningUtil.createSignedAndZippedPkPassArchive(pass, pkPassTemplateInMemory, pkSigningInformation);
}
  1. 将文件发送给前端
private void sendHttpResponse(HttpServletResponse httpServletResponse, ServletOutputStream out, byte[] signedAndZippedPkPassArchive) throws IOException {
    httpServletResponse.setHeader("Cache-Control", "no-cache,no-store");
    httpServletResponse.setHeader("Pragma", "no-cache");
    httpServletResponse.setHeader("Expires", "0");
    httpServletResponse.setHeader("Content-Disposition", "attachment; filename=\"pass.pkpass\"");
    httpServletResponse.setContentType("application/vnd.apple.pkpass");
    httpServletResponse.setContentLength(signedAndZippedPkPassArchive.length);
    httpServletResponse.setHeader("last-modified", String.valueOf(System.currentTimeMillis()));
    out.write(signedAndZippedPkPassArchive);
    out.flush();
}

2.3 注意事项

  1. 卡片标题会自动变为大写;
  2. 设置了过期日期的卡片如果过期了则不支持更新
  3. 卡片本身可以设置多语言等丰富形式,建议可以找一些第三方设计平台设计好了,然后解压通行证文件解析有哪些字段;
  4. IPAD 设备不支持礼品卡

三、更新通行证

3.1 理论知识

  • 通行证的更新要比通行证的创建要复杂一点。
    • 更新逻辑是:我们在创建通行证时设置我们服务器的回调地址:pass.setWebServiceURL(new URL("https://xxxx:xxx/"));
    • 用户下拉刷新会直接调用我们获取最新通行证接口
    • 如果是服务器推送的话我们会先发送设备token给苹果APNS,然后APNS会通知对应的设备来查询我们是否存在需要更新的通行证接口,如果有的话,就会继续调用获取最新通行证接口达到更新目的;
    • 当用户删除通行证时,我们也需要将它的设备token和绑定信息删除掉,以便于下次重新添加时注册;
  • 如果增加用户在Apple Wallet 下拉刷新的功能,那么只需要提供一个下拉刷新的接口即可,它的全路径是: 回调服务器地址/v1/passes/{passTypeIdentifier}/{serialNumber},在这个接口中Apple 会传入要更新的通行证id,我们则根据通行证id查询到该通行证绑定的业务最新数据,并重新生成通行证(重复之前创建通行证的部分步骤)发送给Apple 设备
  • 如果增加服务器推送,让对应用户的对应设备自动调用获取最新通行证接口(也就是步骤2的接口),我们需要增加五个接口来完成这一个完整的功能;它们分别是:
    • 注册设备接口,当添加时只要设置了回调地址,客户手机会自动访问这个接口将他的设备token发送给我们进行保存;
    • 检查是否存在需要更新的通行证接口,服务器推送更新到APNS时,用户手机会访问这个接口来检查是否存在需要被更新的通行证,如果存在的话,就会调用获取最新通行证接口;
    • 获取最新通行证接口,这个接口会像创建通行证一样,获取最新的信息并返回;它跟手动下拉刷新访问的接口是同一个
    • 删除设备接口,在这个里面我们需要删除对应的设备信息,以便于下次的注册使用;
  • 这里描述可能有一些宽泛,同学们可以查看官方文档以获取详细内容:
    • 接口设计规则
    • 通行证设计、创建、更新指南

3.2 代码演示


import com.xxx.service.DemoService;
import com.xxx.service.RequestBodyUtils;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.ImmutableList;
import com.turo.pushy.apns.PushNotificationResponse;
import com.turo.pushy.apns.util.SimpleApnsPushNotification;
import com.turo.pushy.apns.util.concurrent.PushNotificationFuture;
import de.brendamour.jpasskit.apns.PKSendPushNotificationUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.HttpRequestHandlerServlet;
import springfox.documentation.annotations.ApiIgnore;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.Date;
import java.util.List;

@RestController
@RequestMapping
@Api(tags = {"ceshi"})
public class DemoController {

    @Autowired
    private DemoService demoService;

    @GetMapping("addWallet")
    @ResponseBody
    @ApiOperation("添加到苹果钱包中")
    public void addWallet(@ApiIgnore HttpRequestHandlerServlet httpRequest, @ApiIgnore HttpServletResponse httpResponse) {
        // 根据之前的创建通行证生成完善这个接口就可以;
    }


    /**
     * 注册设备以接收通行证的推送通知
     *
     * @param deviceLibraryIdentifier 用于在未来请求中识别和验证此设备的唯一标识符。
     * @param passTypeIdentifier      通行证的类型,在通行证中指定。
     * @param serialNumber            通行证的序列号,在通行证中指定。
     * @param version                 协议版本——当前为 v1。
     *                                说明:此处可能用于将设备编号等信息记录;
     *                                header : 提供了 Authorization 标头;它的值是单词ApplePass,后跟一个空格,然后是通行证中指定的通行证的授权令牌。
     *                                这里授权令牌看要不要进行保存;
     *                                如果此设备的序列号已注册,则返回 HTTP 状态 200。
     *                                如果注册成功,则返回 HTTP 状态 201。
     *                                如果请求未授权,则返回 HTTP 状态 401。
     *                                否则,返回适当的标准 HTTP 状态。
     *                                这里可能要创建一个设备表,用于记录用户是否已经注册,如果注册成功则返回success
     */
    //Registering a Device to Receive Push Notifications for a Pass
    @PostMapping("v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}")
    @ApiOperation("注册设备")
    public ResponseEntity registrationWallet(@PathVariable("deviceLibraryIdentifier") String deviceLibraryIdentifier,
                                             @PathVariable("passTypeIdentifier") String passTypeIdentifier,
                                             @PathVariable("serialNumber") String serialNumber,
                                             @RequestBody PushToken pushToken
    ) {
        // 这里的pushToken需要保存起来,然后推送更新时,将这个pushToken带上即可;
        System.err.println("time:"+ new Date());
        System.err.println("[暗余]. 注册设备");
        System.err.println("deviceLibraryIdentifier:"+deviceLibraryIdentifier);
        System.err.println("passTypeIdentifier:"+passTypeIdentifier);
        System.err.println("serialNumber:"+serialNumber);
        System.err.println("pushToken:"+ JSON.toJSONString(pushToken));
        return new ResponseEntity(HttpStatus.OK);
    }



    /**
     * 获取与设备关联的通行证的序列号
     *
     * @param version                 协议版本——当前为 v1。
     * @param deviceLibraryIdentifier 用于识别和验证设备的唯一标识符。
     * @param passTypeIdentifier      通行证的类型,在通行证中指定。
     * @param tag                     来自先前请求的标签。(可选的)
     *                                如果passesUpdatedSince参数存在,则仅返回自 指示的时间以来已更新的通行证tag。否则,返回所有通行证。
     * @return 如果有匹配的通行证,则返回 HTTP 状态 200 以及带有以下键和值的 JSON 字典:
     * lastUpdated(细绳)
     * 当前的修改标签。
     * <p>
     * serialNumbers(字符串数组)
     * 匹配通行证的序列号。
     * <p>
     * 如果没有匹配的通行证,则返回 HTTP 状态 204。
     * 否则,返回适当的标准 HTTP 状态
     */
    // Getting the Serial Numbers for Passes Associated with a Device
    @GetMapping("/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}")
    @ResponseBody
    @ApiOperation("获取与设备关联的通行证的序列号")
    public ResponseEntity getSerialNumber(
            @PathVariable("deviceLibraryIdentifier") String deviceLibraryIdentifier,
            @PathVariable("passTypeIdentifier") String passTypeIdentifier,
            @RequestParam(value = "passesUpdatedSince",required = false) String passesUpdatedSince
    ) {
        // 这里的pushToken需要保存起来,然后推送更新时,将这个pushToken带上即可;
        System.err.println("time:"+ new Date());
        System.err.println("[暗余]. 获取与设备关联的通行证的序列号");
        System.err.println("passTypeIdentifier:"+passTypeIdentifier);

        SerialNumberBo serialNumberBo = new SerialNumberBo();
        serialNumberBo.setLastUpdateTime("123455");
        serialNumberBo.setSerialNumbers(ImmutableList.of("aed1a60b-20fa-4346-8d26-43d8fe14f29d","123","456"));
//        serialNumberBo.setSerialNumbers(ImmutableList.of("123"));
        return new ResponseEntity(serialNumberBo, HttpStatus.OK);
    }

    /**
     *
     * passTypeId 凭证的类型,从服务器中指定
     * serial 服务器分配给凭证模板的序列号。您可以通过调用 xxx 来获取它;getPassbookTemplate
     */
    /**
     * 获取最新版本的通行证
     *
     * @param version            协议版本——当前为 v1。
     * @param passTypeIdentifier 通行证的类型,在通行证中指定。
     * @param serialNumber       通行证中指定的唯一通行证标识符。
     *                           header: 提供了 Authorization 标头;它的值是单词ApplePass,后跟一个空格,然后是通行证中指定的通行证的授权令牌。
     *                           如果请求被授权,则返回 HTTP 状态 200 以及传递数据的有效负载。
     *                           如果请求未授权,则返回 HTTP 状态 401。
     *                           否则,返回适当的标准 HTTP 状态。
     *                           支持此端点上的标准 HTTP 缓存:检查If-Modified-Since标头,如果通道未更改,则返回 HTTP 状态代码 304。
     */
    @GetMapping("/v1/passes/{passTypeIdentifier}/{serialNumber}")
    @ResponseBody
    @ApiOperation("获取最新版本的通行证")
    public void pushWallet(
            @PathVariable("passTypeIdentifier") String passTypeIdentifier,
            @PathVariable("serialNumber") String serialNumber,
            @ApiIgnore HttpServletResponse httpResponse
    ) {
        // 这里的pushToken需要保存起来,然后推送更新时,将这个pushToken带上即可;
        System.err.println("time:"+ new Date());
        System.err.println("[暗余]. 获取最新版本的通行证");

        System.err.println("passTypeIdentifier:"+passTypeIdentifier);
        System.err.println("serialNumber:"+serialNumber);
        demoService.pushWallet(passTypeIdentifier, serialNumber, httpResponse);
    }

    /**
     * @param version                 协议版本——当前为 v1。
     * @param deviceLibraryIdentifier 设备库标识符  用于识别和验证设备的唯一标识符。
     * @param passTypeIdentifier      通行证的类型,在通行证中指定。
     * @param serialNumber            通行证中指定的唯一通行证标识符。
     *                                header: 提供了 Authorization 标头;它的值是单词ApplePass,后跟一个空格,然后是通行证中指定的通行证的授权令牌。
     *                                如果解除关联成功,则返回 HTTP 状态 200。
     *                                如果请求未授权,则返回 HTTP 状态 401。
     *                                否则,返回适当的标准 HTTP 状态。
     *                                <p>
     *                                服务器解除指定设备与通行证的关联,当通行证发生变化时,不再向该设备发送推送通知。
     */
//    注销设备
    @ResponseBody
    @DeleteMapping("/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}")
    @ApiOperation("注销设备")
    public ResponseEntity unregisteringDevice(
            @PathVariable("version") String version,
            @PathVariable("deviceLibraryIdentifier") String deviceLibraryIdentifier,
            @PathVariable("passTypeIdentifier") String passTypeIdentifier,
            @PathVariable("serialNumber") String serialNumber
    ) {
        // 这里的pushToken需要保存起来,然后推送更新时,将这个pushToken带上即可;
        System.err.println("time:"+ new Date());
        System.err.println("[暗余]. 注销设备");
        System.err.println("version:"+version);
        System.err.println("deviceLibraryIdentifier:"+deviceLibraryIdentifier);
        System.err.println("passTypeIdentifier:"+passTypeIdentifier);
        System.err.println("serialNumber:"+serialNumber);
        return new ResponseEntity(HttpStatus.OK);
    }

   /**
     * 记录错误日志
     * @param version 版本号
     * @param request 日志信息
     * @return 默认成功
     */nseBody
    @RequestMapping(value = "/v1/log", method = RequestMethod.GET)
    public ResponseEntity<Void> log(
            @RequestBody LogContentRequest request
    ) {
        if (Objects.nonNull(request)) {
            System.out.println(JSON.toJSONString(request));
        }
        return new ResponseEntity<>(HttpStatus.OK);
    }

    public static String getRequestBody() throws IOException {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) attributes;
        HttpServletRequest request = servletRequestAttributes.getRequest();
        BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
        //读取输入流的内容转换为String类型IOUtils必须引入org.apache.dubbo.common.utils.IOUtils;包
        String body = RequestBodyUtils.read(reader);
        return body;
    }


    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    static class SerialNumberBo implements Serializable{
        private String lastUpdateTime;
        private List<String> serialNumbers;
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
   static class LogContentRequest implements Serializable {
        private List<String> logs;
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
   static class PushToken implements Serializable {
        private String pushToken;
    }

    // 主动推送通知
    public static void main(String[] args) throws IOException {

        String privateKeyPath = "D:\\xxx\\passtype_key_dev.p12"; // the private key you exported from keychain
        String privateKeyPassword = "xxxx "; // the password you used to exportprivateKeyPath
        String pushToken = "xxx";
        PKSendPushNotificationUtil pkSendPushNotificationUtil = new PKSendPushNotificationUtil(privateKeyPath, privateKeyPassword);
        PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>> simpleApnsPushNotificationPushNotificationResponsePushNotificationFuture =
                pkSendPushNotificationUtil.sendPushNotificationAsync(pushToken);
        SimpleApnsPushNotification pushNotification = simpleApnsPushNotificationPushNotificationResponsePushNotificationFuture.getPushNotification();
        System.err.println(JSON.toJSONString(pushNotification));
    }
}

3.3 注意事项

  • 通行证更新,需要使用https接口,并且证书认证应该是可信机构认证的;如果本地想进行测试的话,可以使用http,但是Iphone手机需要连接电脑开启开发者权限,在手机的开发者选项中允许HTTP请求即可;
  • 苹果定义的这几个接口,实际里面的内容需要我们根据我们自己的业务去完善。也就是说,从负责的内容来说,企业需要从生成、更新全流程把控自己的礼品卡,而Apple只提供用户的主动访问接口、APNS的通知,以及保证带了正确安全证书的通行证是可信的。
  • 更新存在延迟甚至丢包,所以不能保证每次都能成功更新,所以每个接口要保证其健壮性;

四、写在最后

4.1 结尾

这篇文章是博主实战得出的一些经验总结,如果有纰漏欢迎指出。写文不易,觉得不错的话麻烦点点关注鼓励一下哦,后续会继续更新原创精品内容!后面会继续讲Google Wallet 的部分,如果小伙伴谁遇到了问题可以留在评论区,我能解答的话会帮忙看一下;笔芯~

4.2 补充说明

以下是我对Apple Wallet 的封装代码,有需要可供参考

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import de.brendamour.jpasskit.PKBarcode;
import de.brendamour.jpasskit.PKField;
import de.brendamour.jpasskit.PKPass;
import de.brendamour.jpasskit.enums.PKBarcodeFormat;
import de.brendamour.jpasskit.enums.PKDateStyle;
import de.brendamour.jpasskit.enums.PKTextAlignment;
import de.brendamour.jpasskit.passes.PKGenericPass;
import de.brendamour.jpasskit.signing.PKFileBasedSigningUtil;
import de.brendamour.jpasskit.signing.PKPassTemplateInMemory;
import de.brendamour.jpasskit.signing.PKSigningException;
import de.brendamour.jpasskit.signing.PKSigningInformation;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidParameterException;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

/**
 * @author csdn 暗余
 */
public  class AppleWallet {

    private final PKPass pkPass;
    private final String serialNumber;
    private final PKGenericPass pkGenericPass;
    private final List<PKField> headerFields;
    private final List<PKField> primaryFields;
    private final List<PKField> secondaryFields;
    private final List<PKField> auxiliaryFields;
    private final List<PKField> backFields;
    private PKSigningInformation pkSigningInformation;

    private final PKPassTemplateInMemory pkPassTemplateInMemory;

    private static final String VALID_ERROR = "The apple pass is NOT Valid!";

    public AppleWallet(String serialNumber) {
        pkPass = new PKPass();
        this.serialNumber = serialNumber;
        pkPass.setSerialNumber(serialNumber);
        pkGenericPass = new PKGenericPass();
        headerFields = Lists.newArrayList();
        primaryFields = Lists.newArrayList();
        secondaryFields = Lists.newArrayList();
        auxiliaryFields = Lists.newArrayList();
        backFields = Lists.newArrayList();
        pkPassTemplateInMemory = new PKPassTemplateInMemory();
    }


    public AppleWallet buildWhole(String backgroundColor, String foregroundColor, String labelColor, String description, String logoText) {
        pkPass.setBackgroundColor(backgroundColor);
        pkPass.setForegroundColor(foregroundColor);
        pkPass.setLabelColor(labelColor);
        pkPass.setDescription(description);
        pkPass.setLogoText(logoText);
        return this;
    }

    public AppleWallet buildQrCode(String message) {
        PKBarcode pkBarcode = new PKBarcode();
        pkBarcode.setFormat(PKBarcodeFormat.PKBarcodeFormatQR);
        pkBarcode.setMessage(message);
        pkBarcode.setMessageEncoding(StandardCharsets.UTF_8);
        pkPass.setBarcodes(ImmutableList.of(pkBarcode));
        return this;
    }

    public AppleWallet buildField(GenericPassEnum genericPassType, String keyField, String label, String value, PKDateStyle timeStyle, PKDateStyle dateStyle, PKTextAlignment pkTextAlignment) {
        if (Objects.nonNull(value) && value.length() > 0) {
            PKField pkField = new PKField();
            pkField.setKey(Optional.ofNullable(keyField).orElse(StringUtils.EMPTY));
            pkField.setLabel(Optional.ofNullable(label).orElse(StringUtils.EMPTY));
            pkField.setValue(value);
            pkField.setTimeStyle(timeStyle);
            pkField.setDateStyle(dateStyle);
            pkField.setTextAlignment(pkTextAlignment);
            addGenericList(genericPassType, pkField);
        }
        return this;
    }

    private void addGenericList(GenericPassEnum genericPassType, PKField pkField) {
        switch (genericPassType) {
            case HEADER:
                headerFields.add(pkField);
                break;
            case PRIMARY:
                primaryFields.add(pkField);
                break;
            case SECONDARY:
                secondaryFields.add(pkField);
                break;
            case AUXILIARY:
                auxiliaryFields.add(pkField);
                break;
            case BACK:
                backFields.add(pkField);
                break;
            default:
                throw new InvalidParameterException("Undefined type!");
        }
    }

    public AppleWallet buildField(GenericPassEnum genericPassType, String keyField, String label, Serializable value, PKTextAlignment pkTextAlignment) {
        PKField pkField = new PKField();
        pkField.setKey(Optional.ofNullable(keyField).orElse(StringUtils.EMPTY));
        pkField.setLabel(Optional.ofNullable(label).orElse(StringUtils.EMPTY));
        pkField.setValue(Optional.ofNullable(value).orElse(StringUtils.EMPTY));
        pkField.setTextAlignment(pkTextAlignment);
        addGenericList(genericPassType, pkField);
        return this;
    }


    public AppleWallet startGenericPass() {
        pkGenericPass.setHeaderFields(headerFields);
        pkGenericPass.setPrimaryFields(primaryFields);
        pkGenericPass.setSecondaryFields(secondaryFields);
        pkGenericPass.setAuxiliaryFields(auxiliaryFields);
        pkGenericPass.setBackFields(backFields);
        pkPass.setGeneric(pkGenericPass);
        return this;
    }

    public AppleWallet addFile(String pkPassTemplateInMemoryType, String url) throws IOException {
        int responseCode = 0;
        URL fileUrl = null;
        if (StringUtils.isNotEmpty(url)) {
            try {
                fileUrl = new URL(url);
                HttpURLConnection con = (HttpURLConnection) fileUrl.openConnection();
                responseCode = con.getResponseCode();
            } catch (Exception e) {
                responseCode = HttpStatus.NOT_FOUND.value();
            }
        }

        if (responseCode < HttpStatus.MULTIPLE_CHOICES.value() && responseCode >= HttpStatus.OK.value()) {
            pkPassTemplateInMemory.addFile(pkPassTemplateInMemoryType, Objects.requireNonNull(fileUrl));
        }
        return this;
    }

    public AppleWallet addFile(String pathInTemplate, InputStream inputStream) throws IOException {
        pkPassTemplateInMemory.addFile(pathInTemplate, inputStream);
        return this;
    }

    public PKPassTemplateInMemory getPKPassTemplateInMemory() {
        return pkPassTemplateInMemory;
    }


    public String getSerialNumber() {
        return serialNumber;
    }

    public byte[] create2Byte() throws PKSigningException {
        PKFileBasedSigningUtil pkSigningUtil = new PKFileBasedSigningUtil();
        return pkSigningUtil.createSignedAndZippedPkPassArchive(pkPass, pkPassTemplateInMemory, pkSigningInformation);
    }

    public AppleWallet buildWholeParent(String passTypeIdentifier, String authenticationToken, String teamIdentifier, String organizationName, PKSigningInformation pkSigningInformation) {
        this.pkSigningInformation = pkSigningInformation;
        pkPass.setPassTypeIdentifier(passTypeIdentifier);
        pkPass.setAuthenticationToken(authenticationToken);
        pkPass.setTeamIdentifier(teamIdentifier);
        pkPass.setOrganizationName(Optional.ofNullable(organizationName).orElse(StringUtils.EMPTY));
        return this;
    }

    public PKPass getPkPass() {
        return pkPass;
    }

    public AppleWallet isValid() {
        if (!pkPass.isValid()) {
            List<String> validationErrors = this.pkPass.getValidationErrors();
            String error = CollectionUtils.isNotEmpty(validationErrors) ? validationErrors.get(0) : VALID_ERROR;
            throw new RuntimeException(error);
        } else {
            return this;
        }
    }

    public AppleWallet setWebServiceURL(String url) {
        try {
            pkPass.setWebServiceURL(new URL(url));
            pkPass.setFormatVersion(1);
        } catch (MalformedURLException e) {
           	
        }
        return this;
    }

    public enum GenericPassEnum {
        HEADER, PRIMARY, SECONDARY, AUXILIARY, BACK, OTHER
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/26191.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++多态

目录 多态的概念 多态的定义和实现 深入理解多态 C11 override 和 final 重载、覆盖(重写)、隐藏(重定义)的对比 抽象类 多态的原理 动态绑定与静态绑定 单继承和多继承关系的虚函数表 多态的概念 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具…

SSM整合(五)

SSM整合之事务管理(一) 1.核心准备工作 1.1 导入spring-tx依赖 <!-- 事务spring-tx --> <dependency><groupId>org.springframework</groupId><artifactId>spring-tx</artifactId><version>5.3.22</version> </dependen…

【附源码】计算机毕业设计JAVA研究生招生信息管理

【附源码】计算机毕业设计JAVA研究生招生信息管理 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; JAVA …

旧苹果短信导入新苹果手机上,iphone短信迁移

概述&#xff1a;随着科技的快速发展&#xff0c;手机的更新换代周期也变得越来越短。那么问题来了&#xff1a;旧苹果短信导入新苹果手机上&#xff1f;相信这是新机主心中的痛点&#xff0c;那么今天小编就来教大家如何解决这个问题。 方法一、使用易我手机数据传输软件转移苹…

D0x-17(anti—Sp17)-3C12/TPGS抗精子蛋白单克隆抗体/维生素E聚乙二醇琥珀酸酯偶联阿霉素研究

下面分享了D0x-17(anti—Sp17)-3C12/TPGS抗精子蛋白单克隆抗体/维生素E聚乙二醇琥珀酸酯偶联阿霉素研究方法&#xff0c;来看&#xff01; D0x-TPGS聚乙二醇1000维生素E琥珀酸酯(TPGS)修饰阿霉素研究方法&#xff1a; 用硫酸铵梯度法制备TPGS修饰的阿霉素脂质体,并对其理化性质…

try/catch/finally的各种情况

众所周知&#xff0c;try语句报错&#xff0c;会执行catch语句&#xff0c;然后执行finally&#xff0c;以下这几种情况&#xff0c;看看会如何输出。 1、try语句中包含return&#xff0c;finally包含输出语句 public static void main(String[] args) {// write your code h…

YOLOX代码、预测(使用摄像头实时预测)及其添加SE注意力前后的实验结果

1. 代码获取 https://github.com/Le0v1n/ml_code/tree/main/ObjectDetection/YOLOX 如果代码对你有用&#xff0c;请star一下❤️ 2. 预测 2.1 图片预测 下载权值文件&#xff1a;https://github.com/Megvii-BaseDetection/YOLOX/releases/download/0.1.1rc0/yolox_s.pth将权…

iOS键盘通知弹框使用小结

项目开发中文本框输入的时候经常会用到键盘弹框遮挡的问题。解决办法就是根据底部键盘弹出的高度动态的改变对应view的位置。这里以多行文本框输入为例&#xff0c;效果图如下。 //第一步&#xff0c;注册监听键盘通知 [[NSNotificationCenter defaultCenter] addObserver:self…

只会 Python 不行,不会 Python 万万不行

当下的环境大家有目共睹&#xff0c;未来一段时间情况如何&#xff0c;想必不少人心里也清楚&#xff0c;技术人走到中年&#xff0c;难免会焦虑&#xff0c;职场上干得不爽&#xff0c;但是跳槽也不容易&#xff0c;加上不少企业裁员&#xff0c;换个满意的工作更是难上加难。…

JavaScript——周技能检测——菜单编辑——2022年11月22日(考完)

JavaScript——周技能检测——菜单编辑——2022年11月22日&#xff08;考完&#xff09; 一、语言和环境 1. 实现语言&#xff1a;JavaScript。 2. 开发环境&#xff1a;VScode。 二、要求 1、在文本框中输入两个操作数和选择运算符后&#xff0c;在页面上显示输出结果。 …

电影《名侦探柯南:万圣节的新娘》观后感

上周看了电影《名侦探柯南&#xff1a;万圣节的新娘》&#xff0c;讲述了一场即将发生在东京涉谷的爆炸案件&#xff0c;引来各方的关注&#xff0c;柯南在参与案件的过程中&#xff0c;找出真凶的故事。 故事属于侦探类&#xff0c;有悬疑反转等要素&#xff0c;还是挺好的。不…

推荐系统实战3——推荐系统中Embedding层工作原理浅析

推荐系统实战3——推荐系统中Embedding层工作原理浅析学习前言什么是Embedding一、为什么要有Embedding二、推荐系统中常见的Embedding处理方式1、字符串形式的输入2、连续值&#xff08;特定范围值&#xff09;的输入三、Embedding的注意点学习前言 Embedding层是推荐系统特征…

盘点一下今年世界杯中国赞助商及联名入圈品牌

作为全世界的超级体育赛事&#xff0c;今年世界杯的ip流量无疑是值得期待的。虽然中国球队缺席&#xff0c;但中国赞助的热情是丝毫不减&#xff0c;中国官方合作伙伴&#xff08;赞助商&#xff09;分别是蒙牛、海信、vivo和万达。除了万达&#xff0c;其余三大品牌都是来自各…

护眼灯真的可以护眼吗?2022护眼台灯该怎样选择

或许很多人看过一些报道&#xff0c;认为护眼灯是智商税&#xff0c;并不能护眼&#xff0c;但是&#xff0c;我们并不能因为一两款劣质灯具的不行就否定整个行业的产品&#xff0c;真正高质量的护眼灯&#xff0c;并没有像相关报道说的那样不堪&#xff0c;相反其光线对人眼具…

微信小程序商城迅速流行的决定因素

随着互联网的不断发展壮大&#xff0c;许多人更倾向于线上购物&#xff0c;享受到足不出户就能送货上门的购物体验&#xff0c;这使得线上商城占据了优势。而微信小程序商城更是广受好评。 小程序商城的经营模式在很大程度上契合了当下的消费需求和消费心理。它的情感链接&…

JavaScript开发工具WebStorm入门教程:如何安装WebStorm

WebStorm是jetbrains旗下一款JavaScript 开发工具&#xff0c;被广大JS开发者誉为"Web前端开发神器""最强大的HTML5编辑器""最智能的JavaSscript IDE"。 本文给大家讲解WebStorm的安装教程&#xff0c;欢迎下载最新版产品体验&#xff01; Web…

算法多重要你还不知道吗?字节大佬把LeetCode前400题的解答笔记都整理好了,头发大佬掉,我们跟着吃经验!

你知道现在LeetCode算法在大厂中的重要性吗&#xff1f; 前几天小编看了一个国内算法大神的短视频&#xff0c;他就在视频中指出了算法对当下无论是生活还是找工作中都是非常重要的&#xff01; 没错这个人就是江湖人称“左神”的左程云老师 小编也简单看了一下一些比较知名互…

定语从句------六级

1.复合句/从句三大类 从句在整个句子中做什么成分&#xff0c;就叫什么从句。 形容词性从句&#xff1a;定语从句&#xff0c;带有谓语的完整的结构在另一个句子中做定语 名词性从句 副词性从句&#xff1a;状语从句 从句典型的标志&#xff0c;带有关系词2.定语从句&#xff1…

selenium⾃动化测试⾯试题及答案,看看你会多少?

说到UI自动化&#xff0c;可能大家和我一样&#xff0c;主要是用Selenium。毕竟Selenium可是UI自动化方面的王者。 而且Selenium 支持多平台&#xff0c;可以模拟真实浏览器&#xff0c;也支持多种浏览器&#xff0c;免费开源&#xff0c;对商业用户也没有任何限制&#xff0c…

分布式任务调度项目xxl-job

xxl-job简介 分布式任务调度项目xxl-job的官网&#xff1a;分布式任务调度平台XXL-JOB 大众点评的分布式任务调度平台&#xff0c;是一个轻量级分布式任务调度平台, 其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线&#xff0c…