在iOS订阅体系中,同一订阅组内的用户可以在不同档位之间切换。当用户发起升级或降级操作时,App Store会在服务器通知的pending_renewal_info数组中记录相关状态,其中upgrade_downgrade_choice字段专门用于描述用户做出的降级选择。很多服务端开发者在处理订阅状态同步时只关注auto_renew_status和expiration_intent,却忽略了级别变更的细节,导致用户权益在切换档位后出现错乱。本文将系统解析这一字段以及订阅组内级别变更的完整逻辑。

一、订阅组内升级与降级的基本规则
苹果规定,功能相似的订阅必须放在同一个订阅组里,同一订阅组内同一时刻用户只能拥有一个有效订阅。用户在不同档位之间切换时,行为规则并不相同,这是理解后续字段的前提。
升级(Upgrade)指切换到更高级别的订阅,例如从月度标准版切到月度高级版。升级会立即生效:旧订阅被立即取消,剩余价值按比例折算,新订阅马上开始计费周期。此时服务器会收到一个DID_CHANGE_RENEWAL_PREF类型的通知,通知中不包含upgrade_downgrade_choice,因为升级已经即时完成,无需等待。
降级(Downgrade)则完全不同。用户从高级版切到标准版时,切换不会立即生效,而是要等到当前计费周期结束后才按新档位续订。苹果这样做是为了保护已支付的费用。在这段等待期内,用户的高级版权益依然有效,但服务端需要提前知道用户已经选择了降级,以便在到期时正确切换权益——这正是upgrade_downgrade_choice存在的意义。跨订阅组购买则不属于升级降级,相当于独立的订阅交易,两个订阅可以并存。
二、upgrade_downgrade_choice字段结构解析
upgrade_downgrade_choice出现在App Store服务器通知(v1和v2)以及验证收据接口返回的JSON中,它位于pending_renewal_info数组内。下面是v1通知中一个典型片段:
{
"notification_type": "DID_CHANGE_RENEWAL_PREF",
"latest_receipt_info": [ ... ],
"pending_renewal_info": [
{
"product_id": "com.example.premium.monthly",
"auto_renew_product_id": "com.example.standard.monthly",
"auto_renew_status": "1",
"expiration_intent": "0",
"upgrade_downgrade_choice": {
"product_id": "com.example.standard.monthly",
"promotion_offer_id": ""
}
}
]
}该字段包含两个子键:product_id表示用户选择在下一个周期切换到的目标产品ID,promotion_offer_id表示该切换是否与促销优惠关联,如果没有关联则为空字符串。需要注意,只有当用户选择了降级(或同级别切换)且尚未生效时,这个字段才会出现。一旦新周期开始、降级实际生效,后续通知中此字段会消失,auto_renew_product_id则变更为新的产品ID。
判断用户操作的思路可以归纳为:若通知为DID_CHANGE_RENEWAL_PREF且pending_renewal_info中存在upgrade_downgrade_choice,说明用户选择了降级;若该通知中不存在此字段但auto_renew_product_id已变化且生效时间立即,则说明是升级。服务端必须把这两种情况分开处理,否则容易出现降级用户提前失去高级权益的问题。
三、服务端处理级别变更的完整流程
建议服务端维护一张订阅状态表,至少包含用户ID、当前生效产品ID、待生效产品ID和生效时间四个字段。收到DID_CHANGE_RENEWAL_PREF通知时的处理伪代码如下:
func handleRenewalPrefChange(notice *AppStoreNotification) {
info := notice.PendingRenewalInfo[0]
if info.UpgradeDowngradeChoice != nil {
// 降级:记录待生效档位,当前权益保持不变
targetProduct := info.UpgradeDowngradeChoice.ProductID
SavePendingChange(notice.UserID, targetProduct,
notice.CurrentPeriodEnd)
// 不修改当前生效的产品,继续提供服务
} else {
// 升级:立即切换生效档位
newProduct := info.AutoRenewProductID
ApplyProductImmediately(notice.UserID, newProduct)
}
}等到下一个续订周期开始时,苹果会推送DID_RENEW通知,此时latest_receipt_info中的product_id已经是降级后的新档位。服务端收到后应将待生效记录落盘为当前生效产品,并清理upgrade_downgrade_choice相关的临时状态。这个两阶段确认机制能有效防止用户在等待期内反悔(可以再次切换档位)导致的状态错乱。
另外要处理边界情况:用户在降级等待期内取消订阅(DID_CHANGE_RENEWAL_STATUS通知且auto_renew_status为0),此时降级永远不会生效,服务端应清除待生效记录,让用户在当前周期内继续享受高级版权益直到到期。对于App Store服务器通知v2版本,对应的是subscription_renewal_preference变化,语义与v1一致,逻辑可以复用。
四、常见误区与验证建议
最常见的错误是收到降级通知后立即切换权益,这会让付费到月底的用户瞬间丢失高级功能,引发大量客诉。正确做法是降级只记录不生效。其次是误以为auto_renew_product_id等于当前生效产品,实际上它表示下次续订时将使用的产品,两者在降级等待期内是不同的。
在沙盒环境测试时,可以用一个订阅组内配置两个不同价位的档位,先购买高档位再降级,观察DID_CHANGE_RENEWAL_PREF通知中的字段变化。沙盒续订周期被大幅压缩(月订阅约5分钟),方便快速验证完整的降级生效链路。建议将每次收到的通知原始JSON都落库存档,便于排查状态不一致问题时回溯完整的事件序列。
iOS内购pending_renewal_info订阅升级降级修改时间:2026-08-31 04:18:32