SaaS 订阅: Apple & Google 支付架构设计

一、订阅系统整体架构设计

1.1 核心设计原则

订阅系统的设计需要遵循以下核心原则:

统一抽象层

  • 将 Apple App Store 和 Google Play 的订阅机制抽象为统一的订阅模型
  • 屏蔽不同平台的差异,提供一致的业务接口
  • 支持未来扩展其他支付渠道(如支付宝、微信等)

状态机管理

  • 订阅状态:INITIAL_BUY(首次购买)、RENEWAL(续费)、CANCEL(取消)、EXPIRED(过期)、BILLING_RETRY(计费重试)
  • 订单状态:PENDING(待处理)、PROCESSING(处理中)、SUCCESS(成功)、FAILED(失败)、REFUNDED(退款)
  • 通过状态机确保状态转换的合法性和可追溯性

幂等性保证

  • 所有支付相关接口必须支持幂等性
  • 使用订单号或交易号作为幂等键
  • 防止重复处理导致的业务异常

最终一致性

  • 支付回调与业务确认采用异步处理
  • 通过消息队列和重试机制保证最终一致性
  • 允许短暂的状态不一致,但最终必须一致

1.2 系统分层架构

┌─────────────────────────────────────────┐
│         业务应用层(App/Web)            │
│  - 订阅商品展示                          │
│  - 订阅购买流程                          │
│  - 订阅状态查询                          │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│         订阅服务层(Subscription Service)│
│  - 订阅商品管理                          │
│  - 订阅生命周期管理                      │
│  - 订阅状态同步                          │
│  - 账号关联管理                          │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│         支付网关层(Payment Gateway)    │
│  - 统一支付接口                          │
│  - 平台适配器(Apple/Google)            │
│  - 支付回调处理                          │
│  - 支付确认处理                          │
└─────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────┐
│         数据持久层(Database)           │
│  - 订阅表(subscriptions)               │
│  - 订单表(orders)                      │
│  - 交易记录表(transactions)            │
│  - 账号关联表(account_bindings)        │
└─────────────────────────────────────────┘

二、统一支付网关设计

2.1 网关抽象层

统一支付网关的核心是抽象出通用的订阅模型,将不同平台的差异封装在适配器中。

统一订阅模型

  • subscription_id:平台订阅ID(Apple的original_transaction_id,Google的purchase_token
  • product_id:商品ID(如premium_monthly
  • platform:平台类型(applegoogle
  • status:订阅状态
  • expires_at:过期时间
  • auto_renew:是否自动续费

平台适配器模式

  • Apple适配器:处理App Store Server Notifications、验证receipt、处理JWT
  • Google适配器:处理Google Play Billing、验证purchase token、处理Pub/Sub消息
  • 每个适配器实现统一的接口,便于扩展和维护

2.2 支付流程设计

标准支付流程

  1. 客户端发起订阅请求 → 订阅服务层
  2. 订阅服务层调用支付网关 → 生成统一订单号
  3. 支付网关调用平台SDK → 返回平台交易信息
  4. 客户端完成平台支付 → Apple/Google处理支付
  5. 平台发送支付回调 → 支付网关接收
  6. 客户端确认支付 → 支付网关确认接口
  7. 支付网关处理订单 → 更新订阅状态

三、Apple 订阅机制详解

3.1 Apple订阅协议的本质

重要认知:订阅是用户与Apple Store之间的协议

在深入理解Apple订阅机制之前,必须明确一个核心概念:

  • 订阅的法律主体是Apple,不是业务应用
  • 用户使用Apple ID登录App Store,使用Apple ID绑定的支付方式支付
  • 订阅协议是用户与Apple Store签订的,业务应用只是订阅服务的提供方
  • 这意味着:Apple层面不支持多账号,一个Apple ID对应一个订阅

业务层面的多账号是产品定义

  • 虽然Apple层面不支持多账号,但业务层面可以通过产品设计实现
  • 例如:订阅共享、账号迁移、家庭共享等功能
  • 这些是业务层的产品功能,不是Apple的原生支持

3.2 App Store Server Notifications

Apple通过App Store Server Notifications(V2)推送订阅状态变更,支持两种方式:

Server-to-Server Notifications(推荐)

  • Apple服务器主动推送JSON Web Token(JWT)到我们的服务器
  • 需要配置回调URL和共享密钥
  • 实时性高,可靠性强

Transaction History API(轮询)

  • 定期调用Apple API获取交易历史
  • 作为Server-to-Server的补充,用于数据同步和修复

3.3 订阅事件类型

Apple的订阅通知包含多种事件类型,每种类型对应不同的业务场景:

INITIAL_BUY

  • 首次购买订阅
  • 需要创建新订阅记录
  • 激活用户权益

DID_RENEW

  • 自动续费成功
  • 更新订阅过期时间
  • 延长用户权益

DID_FAIL_TO_RENEW

  • 续费失败(余额不足、支付方式失效等)
  • 订阅进入宽限期(Grace Period)
  • 需要通知用户更新支付方式

CANCEL

  • 用户主动取消订阅
  • 订阅在当前周期结束后失效
  • 需要标记为不自动续费

EXPIRED

  • 订阅已过期
  • 用户权益需要降级
  • 清理相关资源

BILLING_RETRY

  • Apple正在重试计费
  • 订阅处于宽限期内
  • 需要监控重试结果

REFUND

  • 退款事件
  • 需要撤销用户权益
  • 记录退款原因

3.4 订单数据传递机制

关键字段说明

original_transaction_id

  • Apple订阅的唯一标识符
  • 同一订阅的所有交易(首次购买、续费、退款)共享同一个original_transaction_id
  • 用于关联订阅的所有历史记录

transaction_id

  • 单次交易的唯一标识
  • 每次续费都会生成新的transaction_id
  • 用于防止重复处理

app_account_token

  • 应用自定义账号标识
  • 在发起购买时通过SKProductappAccountToken属性传递
  • 用于关联Apple订阅与业务账号
  • 重要限制:Apple限制为64字符,且存在缓存问题(见下文)

web_order_line_item_id

  • 订单行项目ID
  • 用于关联同一订单中的多个商品

数据传递流程

  1. 客户端发起购买

    • App调用StoreKit API发起订阅购买
    • 设置appAccountToken为业务账号ID(如用户ID)
    • Apple返回transaction_idoriginal_transaction_id
  2. Apple处理支付

    • Apple验证支付方式并扣款
    • 生成交易记录
    • appAccountToken与交易关联
  3. 回调通知

    • Apple发送Server Notification
    • JWT中包含完整的交易信息
    • 包含appAccountToken用于账号关联
  4. 服务端处理

    • 解析JWT获取交易信息
    • 通过appAccountToken识别业务账号
    • 通过original_transaction_id关联订阅记录

四、账号关联与多账号处理

4.1 Apple订阅协议的本质

重要认知:订阅是与Apple Store签订的协议

这是理解账号关联的关键前提:

  • Apple订阅是用户与Apple Store之间的协议,不是与业务应用之间的协议
  • 用户使用Apple ID登录App Store,使用Apple ID绑定的支付方式支付
  • 订阅的法律主体是Apple,业务应用只是订阅的服务提供方
  • 这意味着:Apple层面不支持多账号,一个Apple ID对应一个订阅

业务层面的多账号是产品定义

虽然Apple层面不支持多账号,但业务层面可以通过产品设计实现多账号:

  • 账号共享:一个Apple订阅可以授权给多个业务账号使用
  • 账号迁移:用户更换业务账号时,可以将订阅迁移到新账号
  • 家庭共享:通过Apple的家庭共享功能,实现订阅共享

4.2 appAccountToken的局限性

核心问题:Apple账号切换时的缓存问题

这是订阅系统中最容易出错的场景:

问题场景

  1. 用户A使用Apple ID A登录,购买订阅,appAccountToken传递订单号order_001
  2. 用户A退出登录,用户B使用Apple ID B登录
  3. 用户B购买订阅时,Apple可能返回缓存的appAccountToken,仍然是order_001
  4. 导致订单号错误关联到用户B,而不是用户A

根本原因

  • appAccountToken是应用层传递的,但Apple会缓存这个值
  • 当切换Apple账号时,Apple可能使用缓存的token,而不是当前账号的token
  • 这是Apple Store的机制,应用层无法完全控制

解决方案

方案一:不依赖appAccountToken传递订单号

  • appAccountToken只传递用户ID,不传递订单号
  • 服务端维护"用户ID + 时间窗口"的订单映射表
  • 通过时间窗口匹配订单,而不是依赖token中的订单号
  • 优点:避免缓存问题
  • 缺点:需要额外的映射逻辑

方案二:客户端验证机制

  • 客户端在确认支付时,验证appAccountToken是否匹配当前用户
  • 如果不匹配,提示用户重新购买或联系客服
  • 优点:及时发现错误
  • 缺点:用户体验不佳

方案三:服务端二次验证

  • 服务端收到回调时,验证appAccountToken中的用户ID
  • 如果用户ID不匹配,记录异常并告警
  • 通过人工或自动流程修复关联关系
  • 优点:保证数据正确性
  • 缺点:需要额外的修复机制

推荐方案:组合使用

  • appAccountToken传递用户ID(不传递订单号)
  • 服务端维护订单与用户的映射关系
  • 客户端和服务端都进行验证
  • 定期对账,发现异常及时修复

4.3 账号关联机制

一对一关联(标准场景)

  • 一个Apple订阅对应一个业务账号
  • 通过appAccountToken中的用户ID关联
  • 适用于大多数单用户场景

一对多关联(订阅共享)

  • 一个Apple订阅可以授权给多个业务账号使用
  • 通过业务层的授权机制实现
  • 需要额外的授权表记录授权关系
  • 适用于家庭共享、团队订阅等场景

多对一关联(账号迁移)

  • 用户更换业务账号时,可以将订阅迁移到新账号
  • 需要验证订阅的所有权(通过Apple ID验证)
  • 更新账号关联表,保留订阅历史
  • 适用于用户账号迁移场景

4.4 多账号处理策略

账号识别方案

方案一:appAccountToken只传递用户ID

  • appAccountToken格式:user_id(如user_12345
  • 服务端维护订单与用户的映射关系
  • 通过时间窗口匹配订单
  • 优点:避免缓存问题,灵活
  • 缺点:需要额外的映射表

方案二:appAccountToken传递加密的用户标识

  • 将用户ID加密后传递
  • 服务端解密获取用户ID
  • 优点:安全性高
  • 缺点:需要密钥管理

推荐方案:方案一

  • 简单直接,避免缓存问题
  • 通过映射表管理订单与用户的关系
  • 支持账号迁移和订阅共享

4.5 账号关联表设计

账号关联表需要记录以下信息:

  • subscription_id:平台订阅ID(original_transaction_id
  • platform:平台类型
  • apple_id:Apple ID(用于验证订阅所有权)
  • user_id:业务用户ID(主账号)
  • app_account_token:Apple传递的账号token(用于关联)
  • status:关联状态(activetransferredcancelled
  • created_atupdated_at:时间戳

订单映射表设计

  • order_id:业务订单号
  • user_id:业务用户ID
  • transaction_id:平台交易ID
  • created_at:订单创建时间
  • 用于通过时间窗口匹配订单与用户

4.6 账号切换的处理流程

正常流程

  1. 用户A登录,购买订阅
  2. appAccountToken传递用户A的ID
  3. 服务端创建订单,关联用户A
  4. 订阅激活,用户A获得权益

异常流程(账号切换)

  1. 用户A登录,购买订阅(订单号order_001
  2. 用户A退出,用户B登录
  3. 用户B购买订阅,但Apple返回缓存的appAccountToken(用户A的ID)
  4. 服务端检测到appAccountToken不匹配当前用户B
  5. 记录异常,告警
  6. 通过时间窗口匹配订单,正确关联到用户B
  7. 修复关联关系

预防措施

  • 客户端在购买前清除可能的缓存
  • 服务端验证appAccountToken与当前用户的匹配性
  • 定期对账,发现异常及时修复
  • 记录所有账号切换的日志,便于问题排查

五、支付回调与确认接口设计

5.1 回调接口设计

接口职责

  • 接收Apple/Google的支付通知
  • 验证通知的合法性(签名验证、JWT验证)
  • 解析通知内容
  • 异步处理订单更新

处理流程

  1. 接收回调请求
  2. 验证签名/JWT
  3. 解析通知数据
  4. 获取分布式锁(基于订单号或交易号)
  5. 检查订单状态(幂等性检查)
  6. 更新订单状态为PROCESSING
  7. 发送消息到消息队列
  8. 释放分布式锁
  9. 返回200状态码(告知平台已接收)

注意事项

  • 必须快速响应(200状态码),避免平台重试
  • 业务处理异步化,避免阻塞回调
  • 记录所有回调日志,便于问题排查
  • 支持重试机制,处理临时失败

5.2 确认接口设计

接口职责

  • 接收客户端的支付确认请求
  • 验证客户端传递的交易信息
  • 与回调接口协同完成订单处理
  • 返回最终的订单状态

处理流程

  1. 接收确认请求(包含订单号、交易号等)
  2. 获取分布式锁(与回调接口使用相同的锁键)
  3. 查询订单当前状态
  4. 如果订单已处理,直接返回结果
  5. 如果订单未处理,等待回调处理或主动查询平台
  6. 更新订单状态为SUCCESS
  7. 更新订阅状态
  8. 激活用户权益
  9. 释放分布式锁
  10. 返回确认结果

客户端调用时机

  • 支付成功后立即调用
  • 应用启动时检查未确认的订单
  • 定期轮询未确认的订单状态

5.3 分布式锁设计

锁的作用

  • 防止回调接口和确认接口并发处理同一订单
  • 保证订单处理的原子性
  • 避免重复处理导致的业务异常

锁的实现

  • 使用Redis分布式锁
  • 锁的Key:payment_lock:{platform}:{order_id}payment_lock:{platform}:{transaction_id}
  • 锁的TTL:30秒(根据业务处理时间调整)
  • 锁的获取:使用SETNXSET命令,设置过期时间

锁的使用场景

场景一:回调先到,确认后到

  1. 回调接口获取锁,开始处理
  2. 确认接口尝试获取锁,失败,等待或返回处理中
  3. 回调接口处理完成,释放锁
  4. 确认接口重新获取锁,发现已处理,直接返回结果

场景二:确认先到,回调后到

  1. 确认接口获取锁,查询订单状态
  2. 订单未处理,等待回调或主动查询平台
  3. 回调接口到达,尝试获取锁,失败,等待
  4. 确认接口处理完成,释放锁
  5. 回调接口获取锁,发现已处理,跳过处理

场景三:同时到达

  1. 只有一个接口能获取锁
  2. 另一个接口等待或返回处理中
  3. 获取锁的接口处理完成后,另一个接口再处理

锁的优化

  • 使用可重入锁,避免同一请求重复获取
  • 设置合理的等待时间,避免无限等待
  • 记录锁竞争情况,监控系统性能
  • 支持锁的续期,处理长时间业务操作

六、订阅与订单的本质区别

6.1 核心认知:订阅是链条,订单是节点

这是订阅系统设计中最容易被误解的概念。订阅和订单是两种完全不同的东西,理解它们的本质区别是设计订阅系统的前提。

订单的本质:离散的交易记录

  • 订单是一次性的、独立的交易事件
  • 每个订单都有明确的开始和结束
  • 订单是点状的,代表某个时间点的支付行为
  • 订单的生命周期:创建 → 支付 → 完成/失败/退款

订阅的本质:连续的服务链条

  • 订阅是持续性的、有状态的服务关系
  • 订阅没有明确的结束(除非主动取消或过期)
  • 订阅是线状的,代表一段时间的服务权益
  • 订阅的生命周期:激活 → 续费 → 续费 → ... → 取消/过期

6.2 产品经理的常见误区

误区一:用订单思维理解订阅

  • ❌ 错误认知:订阅就是"一个长期订单"
  • ✅ 正确认知:订阅是"一个由多个订单组成的服务链条"

误区二:用订单状态推导订阅状态

  • ❌ 错误做法:订单成功 = 订阅成功,订单失败 = 订阅失败
  • ✅ 正确做法:订阅状态需要综合所有订单的状态,考虑续费周期、宽限期等因素

误区三:用订单ID管理订阅

  • ❌ 错误做法:用订单号作为订阅的唯一标识
  • ✅ 正确做法:用original_transaction_id(Apple)或purchase_token(Google)作为订阅标识

6.3 订阅与订单的关系模型

一对多关系:一个订阅包含多个订单

订阅链条(Subscription Chain)
├── 订单1:INITIAL_BUY(首次购买)
│   └── 状态:SUCCESS
├── 订单2:RENEWAL(第1次续费)
│   └── 状态:SUCCESS
├── 订单3:RENEWAL(第2次续费)
│   └── 状态:FAILED(续费失败)
├── 订单4:BILLING_RETRY(计费重试)
│   └── 状态:SUCCESS(重试成功)
├── 订单5:RENEWAL(第3次续费)
│   └── 状态:SUCCESS
└── 订单6:REFUND(退款)
    └── 状态:SUCCESS

关键理解

  • 订阅的original_transaction_id贯穿整个链条,所有订单共享这个ID
  • 每个订单都有独立的transaction_id,用于防止重复处理
  • 订阅的状态由整个链条的状态决定,不是单个订单的状态

6.4 订单类型与订阅状态的关系

订单类型

  • INITIAL:首次购买订单,创建订阅链条
  • RENEWAL:续费订单,延长订阅链条
  • REFUND:退款订单,可能终止订阅链条
  • UPGRADE:升级订单,改变订阅商品
  • DOWNGRADE:降级订单,改变订阅商品

订阅状态推导逻辑

订阅状态不能简单地从单个订单状态推导,需要考虑:

  1. 订阅激活条件

    • 至少有一个SUCCESS状态的订单(INITIAL或RENEWAL)
    • 且订阅未过期
  2. 订阅过期条件

    • 最后一个有效订单已过期
    • 且没有新的续费订单
    • 且宽限期已结束
  3. 订阅取消条件

    • 用户主动取消(CANCEL事件)
    • 订阅在当前周期结束后失效
    • 但当前周期内的权益仍然有效
  4. 订阅宽限期条件

    • 续费失败(DID_FAIL_TO_RENEW)
    • 但Apple正在重试(BILLING_RETRY)
    • 宽限期内权益仍然有效

6.5 设计建议

数据模型设计

  • 订阅表(subscriptions):存储订阅链条的元信息

    • subscription_id:订阅唯一标识(original_transaction_id
    • status:订阅状态(ACTIVE、EXPIRED、CANCELLED等)
    • expires_at:过期时间
    • auto_renew:是否自动续费
  • 订单表(orders):存储链条中的每个节点

    • order_id:订单唯一标识
    • subscription_id:关联的订阅ID
    • transaction_id:平台交易ID
    • order_type:订单类型(INITIAL、RENEWAL等)
    • status:订单状态(SUCCESS、FAILED等)

业务逻辑设计

  • 订阅状态查询:查询订阅表,而不是订单表
  • 订阅续费:创建新订单,更新订阅过期时间
  • 订阅取消:更新订阅状态,不影响当前周期内的订单
  • 订阅退款:创建退款订单,可能终止订阅链条

避免的陷阱

  • ❌ 不要用订单状态直接判断订阅状态
  • ❌ 不要用订单ID作为订阅标识
  • ❌ 不要用订单表替代订阅表
  • ✅ 订阅是主体,订单是订阅的组成部分
  • ✅ 订阅状态需要综合所有订单的状态
  • ✅ 订阅有生命周期,订单只是生命周期中的事件

七、各种回调通知类型及处理方式

7.1 Apple回调通知类型

INITIAL_BUY

  • 触发时机:用户首次购买订阅
  • 处理逻辑
    • 创建订阅记录
    • 创建订单记录(类型:INITIAL)
    • 激活用户权益
    • 设置订阅过期时间
    • 发送欢迎通知

DID_RENEW

  • 触发时机:订阅自动续费成功
  • 处理逻辑
    • 创建订单记录(类型:RENEWAL)
    • 更新订阅过期时间
    • 延长用户权益
    • 发送续费成功通知

DID_FAIL_TO_RENEW

  • 触发时机:续费失败(余额不足、支付方式失效)
  • 处理逻辑
    • 标记订阅进入宽限期
    • 发送续费失败通知
    • 提醒用户更新支付方式
    • 设置宽限期到期时间

CANCEL

  • 触发时机:用户主动取消订阅
  • 处理逻辑
    • 标记订阅为不自动续费
    • 订阅在当前周期结束后失效
    • 发送取消确认通知
    • 记录取消原因(如果提供)

EXPIRED

  • 触发时机:订阅过期(包括宽限期结束)
  • 处理逻辑
    • 更新订阅状态为EXPIRED
    • 撤销用户权益
    • 降级用户等级
    • 发送过期通知
    • 清理相关资源

BILLING_RETRY

  • 触发时机:Apple正在重试计费
  • 处理逻辑
    • 标记订阅处于重试状态
    • 监控重试结果
    • 记录重试次数
    • 如果重试成功,按DID_RENEW处理
    • 如果重试失败,按EXPIRED处理

REFUND

  • 触发时机:用户申请退款或Apple自动退款
  • 处理逻辑
    • 创建退款订单记录
    • 撤销用户权益
    • 记录退款原因
    • 发送退款通知
    • 更新订阅状态

PRICE_INCREASE

  • 触发时机:订阅价格上调
  • 处理逻辑
    • 通知用户价格变更
    • 等待用户确认
    • 如果用户同意,继续订阅
    • 如果用户拒绝,取消订阅

GRACE_PERIOD_EXPIRED

  • 触发时机:宽限期结束
  • 处理逻辑
    • 更新订阅状态为EXPIRED
    • 撤销用户权益
    • 发送过期通知

7.2 Google回调通知类型

SUBSCRIPTION_PURCHASED

  • 触发时机:用户购买订阅
  • 处理逻辑:类似Apple的INITIAL_BUY

SUBSCRIPTION_RENEWED

  • 触发时机:订阅自动续费
  • 处理逻辑:类似Apple的DID_RENEW

SUBSCRIPTION_IN_GRACE_PERIOD

  • 触发时机:订阅进入宽限期
  • 处理逻辑:类似Apple的DID_FAIL_TO_RENEW

SUBSCRIPTION_CANCELED

  • 触发时机:用户取消订阅
  • 处理逻辑:类似Apple的CANCEL

SUBSCRIPTION_EXPIRED

  • 触发时机:订阅过期
  • 处理逻辑:类似Apple的EXPIRED

SUBSCRIPTION_RESTARTED

  • 触发时机:用户在取消后重新订阅
  • 处理逻辑
    • 恢复订阅状态
    • 重新激活用户权益
    • 更新订阅过期时间

SUBSCRIPTION_PRICE_CHANGE_CONFIRMED

  • 触发时机:用户确认价格变更
  • 处理逻辑:类似Apple的PRICE_INCREASE

SUBSCRIPTION_DEFERRED

  • 触发时机:订阅延期(如促销活动)
  • 处理逻辑
    • 延长订阅过期时间
    • 记录延期原因

SUBSCRIPTION_REVOKED

  • 触发时机:订阅被撤销(如退款)
  • 处理逻辑:类似Apple的REFUND

7.3 统一处理策略

事件映射表

  • 将不同平台的事件类型映射到统一的事件类型
  • 例如:Apple的INITIAL_BUY和Google的SUBSCRIPTION_PURCHASED都映射为SUBSCRIBE

处理流程标准化

  • 所有事件都经过统一的处理流程
  • 验证 → 解析 → 幂等性检查 → 业务处理 → 状态更新 → 通知

错误处理

  • 记录所有处理失败的事件
  • 支持手动重试
  • 告警机制,及时发现问题

八、系统监控与运维

8.1 关键指标监控

支付相关指标

  • 支付成功率
  • 支付回调延迟
  • 支付确认延迟
  • 订单处理失败率
  • 分布式锁竞争率

订阅相关指标

  • 订阅激活率
  • 订阅续费率
  • 订阅取消率
  • 订阅过期率
  • 宽限期转化率

系统性能指标

  • API响应时间
  • 数据库查询性能
  • 消息队列堆积情况
  • 缓存命中率

8.2 日志与追踪

日志记录

  • 所有支付回调的完整日志
  • 所有订单状态变更日志
  • 所有订阅状态变更日志
  • 分布式锁获取/释放日志
  • 错误和异常日志

链路追踪

  • 使用TraceID关联同一请求的所有日志
  • 追踪订单从创建到完成的完整流程
  • 识别性能瓶颈和异常点

8.3 数据一致性保障

定期对账

  • 定期与Apple/Google平台对账
  • 发现数据不一致及时修复
  • 记录对账结果和修复操作

数据修复机制

  • 支持手动触发订单重新处理
  • 支持批量修复历史数据
  • 支持数据回滚和补偿

九、最佳实践总结

9.1 设计原则

  1. 统一抽象:将不同平台的差异封装在适配层,业务层无需关心平台差异
  2. 幂等性优先:所有接口必须支持幂等性,防止重复处理
  3. 异步处理:支付回调快速响应,业务处理异步化
  4. 最终一致性:允许短暂不一致,但最终必须一致
  5. 可观测性:完善的日志、监控、追踪机制

9.2 技术选型建议

分布式锁

  • Redis分布式锁(推荐)
  • 或使用数据库悲观锁(性能较低)

消息队列

  • RabbitMQ、Kafka等(用于异步处理)
  • 保证消息的可靠投递

数据库

  • 支持事务的关系型数据库(MySQL、PostgreSQL)
  • 保证数据一致性

缓存

  • Redis(用于分布式锁、缓存订阅状态)
  • 提高查询性能

9.3 常见问题与解决方案

问题一:回调丢失

  • 解决方案:定期轮询平台API,补充缺失的回调
  • 实现Transaction History API的定期同步

问题二:并发冲突

  • 解决方案:使用分布式锁,保证同一订单的串行处理
  • 设置合理的锁超时时间

问题三:账号关联错误(Apple账号切换缓存问题)

  • 问题:切换Apple账号时,appAccountToken可能返回缓存的旧值
  • 解决方案:
    • appAccountToken只传递用户ID,不传递订单号
    • 服务端维护订单与用户的映射关系表
    • 通过时间窗口匹配订单,而不是依赖token中的订单号
    • 客户端和服务端都进行验证,发现异常及时修复

问题四:状态不一致

  • 解决方案:定期对账,发现不一致及时修复
  • 实现状态修复工具

问题五:性能瓶颈

  • 解决方案:异步处理、消息队列、缓存优化
  • 数据库索引优化、读写分离

十、总结

SaaS订阅系统的设计是一个复杂的系统工程,需要综合考虑支付平台差异、账号管理、并发控制、数据一致性等多个方面。通过统一支付网关、分布式锁、异步处理等机制,可以构建一个稳定、可靠、可扩展的订阅系统。

关键要点:

  • 订阅与订单的本质区别:订阅是连续的服务链条,订单是离散的交易节点。不能用订单思维理解订阅,订阅状态需要综合所有订单的状态
  • Apple订阅协议的本质:订阅是用户与Apple Store之间的协议,Apple层面不支持多账号,业务层的多账号是产品定义
  • appAccountToken的局限性:切换Apple账号时存在缓存问题,不能依赖token传递订单号,应该通过映射表管理订单与用户的关系
  • 统一抽象:屏蔽平台差异,提供一致接口
  • 幂等性:防止重复处理,保证数据正确性
  • 并发控制:分布式锁保证订单处理的原子性
  • 事件驱动:通过回调机制实时同步订阅状态
  • 可观测性:完善的监控和日志,快速定位问题

通过遵循这些设计原则和最佳实践,可以构建一个生产级的SaaS订阅系统,为用户提供流畅的订阅体验。最重要的是理解订阅与订单的本质区别,避免用订单思维设计订阅系统。

更新时间:2025年12月25日