在之前的文章《痛苦之源!Apple 内购的运用和服务器规划(一)》中,我介绍了规划 Apple 内购服务器需求用到的各个接口以及相关的文档,而且针对各个接口给了 Python 和 Java 两个版别的示例代码。

在这篇文章中,我将介绍怎么整合这些接口并规划一个 Apple 内购服务器的。

1、全体规划

这儿不论 iPhone 客户端和后端统一运用苹果内购的 v2 版别开发。

注:开发该项目进程中,我运用了之前现已搭建好的服务器。该服务器运用 SpringBoot 开发,假如你希望开发一个自己的服务器,可是又没有满足的经验,你能够运用我根据自己的项目开源的服务器项目 Seed 进行开发。

该内购服务器的全体规划如下所示,

Apple IAP 方案 (2):Apple 内购的服务器设计方案

当时版别,我考虑的流程主要有惯例付出流程、退款流程和前史告诉守时轮询三个。

1.1 惯例付出流程

惯例付出流程指的是从客户端发起购买到 Apple 服务器校验再到咱们自己的服务器校验的进程。可能存在许多运用不依赖于自己的服务器的校验,直接在客户端调用苹果的接口校验订单信息。这样也能够,可是不够安全。这个流程中,咱们会将客户端购买完成效果之后的 产品 ID、运用的 Bundle ID 以及买卖的 ID 传给后端,然后由后端调用 Apple 供给的 V2 版别的接口进行订单信息的校验。

最近也看到一些 Apple 内购破解的机制,其中有一个便是运用伪装的买卖 ID 进行校验。假如咱们的接口内只用买卖 ID 作为判别根据,就容易被黑。所以,这儿需求留意需求将 产品 ID、运用的 Bundle ID 以及买卖的 ID 一同作为判别的根据。

另外,咱们也能够看到根据 Apple 的 v2 版别的接口,比较于根据 v1 版别接口而言,要传输的数据确实少了许多。这对于提高接口的性能至关重要。

1.2 退款处理流程

退款处理流程指的是经过监听来自 Apple 的回调的方法处理退款的流程。咱们能够在 Apple Store Connect 的产品信息中填写暴露给 Apple 的接口的地址。

这儿有个坑,便是供给给 Apple 的接口必须支持 https 协议,不论生产环境还是开发环境。我刚注销了一个域名,而免费 SSL 证书的子域名又无法运用 SSL. 所以,这个在开发环境一直走不通。

在用户发起退款的时分,Apple 会以服务器到服务器的方式向咱们填入的接口发送一个恳求,并顺便一些订单的信息(本质上便是一个 http 恳求)。咱们能够根据该订单信息的内容将用户的指定付费内容设置为不可用即可。

1.3 前史告诉守时轮询

前史告诉轮询和退款处理在做的事情类似。可能是考虑到用户的服务器可能会由于宕机等情形的呈现导致没有及时接收到来自 Apple 服务器的恳求。所以,Apple 给咱们供给了主意向 Apple 服务器查询前史告诉的接口。

因而,根据该接口,咱们能够做如下规划:运用 SpringBoot 的守时使命功能,间隔一段时间主意向 Apple 服务器恳求前史告诉。然后,按照 1.2 中的流程根据告诉的类型做相应的处理即可。

考虑到前史告诉可能会由于一些原因得不到正确的处理,而为了排查问题,我这儿对前史告诉做了落表的处理。以此用于追踪前史告诉的实践处理情况,也作为排查用户客述问题的根据。

2、详细规划

2.1 范畴模型规划(数据结构)

首要,运用信息数据结构 AppInfo* 新增了字段 packageName。该字段用来获取运用对应的 ID 信息,以在恳求对应 IAP 服务器的时分运用。

/**
 * Apple 运用的 Bundle ID
 */
@Column(name = "bundle_id")
@ColumnInfo(comment = "Bundle Id")
private String bundleId;

然后,对产品信息数据库结构 AppGoods 中新增字段 appleGoodsId。这两个字段用来指定产品对应的苹果产品 ID。

@Column(name = "apple_goods_id")
@ColumnInfo(comment = "Apple Goods Id", added = true)
private String appleGoodsId;

然后,对付出信息数据结构 AppPayment 新增三个字段,用来记载苹果付出的信息。这个数据结构主要用来追踪用户的付出信息,

/** 苹果产品 ID */
@Column(name = "apple_product_id")
@ColumnInfo(comment = "Apple Product Id", added = true)
private String appleProductId;
/** 苹果运用的 Bundle ID */
@Column(name = "apple_bundle_id")
@ColumnInfo(comment = "Apple Bundle Id", added = true)
private String appleBundleId;
/** 苹果买卖的 ID */
@Column(name = "apple_transaction_id")
@ColumnInfo(comment = "Apple Transaction Id", added = true)
private String appleTransactionId;

然后,新增前史告诉数据结构 AppleNotification。该数据结构用来耐久化从 Apple Store 查询到的前史告诉信息。这个表结构中界说了一些枚举和类,这儿的枚举没有映射成整数类型,由于内部运用,没做进一步处理。这儿的类类型是以字符串的方式存储到数据库中的,在读取和耐久化的时分会运用 json 进行序列化和反序列化。数据结构参阅文档:notificationhistoryresponse.

@Data
@Table(name = "gt_apple_notification")
@TableInfo(comment = "Apple Notification")
@EqualsAndHashCode(callSuper = true)
public class AppleNotification extends AbstractPo {
    @Column(name = "notification_uuid")
    @ColumnInfo(comment = "Notification uuid")
    private String notificationUUID;
    /**
     * 第一次发送失利的原因
     */
    @Column(name = "first_result")
    @ColumnInfo(comment = "First send result")
    private FirstSendAttemptResult firstResult;
    @Column(name = "notification_type")
    @ColumnInfo(comment = "Notification type")
    private AppleNotificationType notificationType;
    @Column(name = "subtype")
    @ColumnInfo(comment = "Notification subtype")
    private AppleNotificationSubType subtype;
    @Column(name = "data", length = 2000)
    @ColumnInfo(comment = "Notification data")
    private AppleNotification.Data data;
    @Column(name = "summary", length = 2000)
    @ColumnInfo(comment = "Notification summary")
    private AppleNotification.Summary summary;
    @Column(name = "version")
    @ColumnInfo(comment = "Version")
    private String version;
    @Column(name = "signed_date")
    @ColumnInfo(comment = "Singed date")
    private Date signedDate;
    /**
     * 告诉的状态
     */
    @Column(name = "status")
    @ColumnInfo(comment = "Notification status", added = true)
    private Status status;
}

最后,对会员信息数据结构 UserVipInfo* 改动。新增的两个字段别离用来追踪订单信息和禁用某个会员信息(比如用户退款等时分禁用,而不是删去,不能删去,需求保存数据记载)。

/**
 * 生成该会员记载的订单的 ID
 */
@Column(name = "payment_id")
@ColumnInfo(comment = "App Payment Id", added = true)
private Long paymentId;
/**
 * 该会员信息启用/禁用
 */
@Column(name = "enable")
@ColumnInfo(comment = "User vip info enabled or disabled", added = true)
private Boolean enable;

2.2 涉及的 Apple 接口

下面是全体流程规划中用到的 Apple 的接口,

/** 苹果内购订单信息相关 API */
public interface AppleTransactionApi {
    /**
     * 获取订单的前史信息,文档:
     * https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history
     * *
     * @param originalTransactionId 原始的订单 ID
     * @return 前史记载呼应信息
     */
    @GET("/inApps/v1/history/{originalTransactionId}")
    Call<AppleHistoryResponse> getTransactionHistory(
            @Header("Authorization") String authorization,
            @Path("originalTransactionId") String originalTransactionId);
    /**
     * 给 Apple 服务器发送告诉恳求,文档:
     * https://developer.apple.com/documentation/appstoreserverapi/request_a_test_notification
     *
     * @param authorization authorization
     * @return 恳求成果
     */
    @POST("/inApps/v1/notifications/test")
    Call<AppleNotificationTestResponse> testNotification(@Header("Authorization") String authorization);
    /**
     * 获取前史告诉,文档:
     * https://developer.apple.com/documentation/appstoreserverapi/get_notification_history
     *
     * @return 前史告诉呼应
     */
    @POST("/inApps/v1/notifications/history")
    Call<AppleNotificationHistoryResponse> getNotificationHistory(
            @Header("Authorization") String authorization,
            @Body AppleNotificationHistoryRequest request);
    /** 获取前史告诉,带分页参数版别。 */
    @POST("/inApps/v1/notifications/history")
    Call<AppleNotificationHistoryResponse> getNotificationHistory(
            @Header("Authorization") String authorization,
            @Query("paginationToken") String paginationToken,
            @Body AppleNotificationHistoryRequest request);
}

在校验订单信息的时分,咱们运用 /inApps/v1/history 这个接口,然后将从 Apple 查询到的订单的产品 ID、运用 ID 以及买卖 ID 作对比来判别信息是否正确。若正确,则给予用户对应的产品,否则回来错误信息。

而接受 Apple 回调的逻辑则是直接从 Apple 传入的 Payload 中解析前史告诉信息,然后处理该告诉。代码如下,

@Override
public ResponseEntity<Object> handleNotification(String signedPayload) {
    DecodedJWT decodedJWT = JWT.decode(signedPayload);
    DecodedJWTReader reader = new DecodedJWTReader(decodedJWT);
    NotificationHistory.Payload payload = new NotificationHistory.Payload(reader);
    PackVo<Object> handlePackVo = handleNotification(payload);
    // 回来处理的码给 Apple Store 服务器
    if (handlePackVo.isSuccess()) {
        return ResponseEntity.ok().build();
    }
    return ResponseEntity.badRequest().build();
}

留意,回来给 Apple 服务器的成果码的含义。200 则表明这个告诉被正确地处理,否则表明该告诉处理失利。

而前史告诉守时轮询的逻辑则如下所示,以每小时一次的方式恳求前史告诉并处理。

/**
 * 每一小时执行一次的使命:Apple Store 的前史告诉守时查询
 */
@Async(value = "applicationTaskExecutor")
@Scheduled(cron = "0 0 0/1 * * ?")
public void handleAppleStoreNotifications() {
    log.info("Triggered Apple Store timely history notifications.");
    PackVo<Object> packVo = applePayService.handleHistoryNotification();
    if (!packVo.isSuccess()) {
        log.error("Failed to process Apple Store history notifications.");
    }
    }

这儿用了 /inApps/v1/notifications/history 接口,以分页恳求的方式以获取所有的前史告诉信息,

private PackVo<AppleNotificationHistoryResponse> getNotificationHistory(String bundleId) {
    String authorization = getAuthorization(bundleId);
    if (StrUtil.isEmpty(authorization)) {
        return failure(ERROR_APPLE_IAP_FAILED_TO_GET_AUTHORIZATION);
    }
    AppleTransactionApi appleTransactionApi = new Retrofit.Builder()
            .baseUrl(host)
            .addConverterFactory(RetrofitManager.getFactory())
            .build().create(AppleTransactionApi.class);
    Response<AppleNotificationHistoryResponse> response;
    try {
        AppleNotificationHistoryRequest request = new AppleNotificationHistoryRequest();
        long current = System.currentTimeMillis();
        request.setStartDate(current - 24*60*60*1000); // 过去 24 小时
        request.setEndDate(current);
        response = appleTransactionApi.getNotificationHistory(authorization, request).execute();
        AppleNotificationHistoryResponse appleNotificationHistoryResponse = response.body();
        if (response.isSuccessful() && appleNotificationHistoryResponse != null) {
            appleNotificationHistoryResponse.decode();
            appleNotificationService.saveAppleNotificationHistoryResponse(appleNotificationHistoryResponse);
            return PackVo.success(appleNotificationHistoryResponse);
        } else {
            log.error("Failed to request history notification [{}] [{}]", response.code(), response);
            return failure(ERROR_APPLE_IAP_REQUEST_FAILED);
        }
    } catch (IOException e) {
        log.error("Failed to request history notifications from apple server due to IO exception: ", e);
        return failure(ERROR_APPLE_IAP_REQUEST_FAILED);
    }
}

总结

以上便是 Apple 内购服务器的规划方案,主要是供给一个全体的规划思路,实践的代码是和事务结合的,一堆校验逻辑,没有太大的参阅价值。

如有疑问,可在评论区交流。