- 后端
- 文档
【免费下载链接】readthedocs.org
The source code that powers readthedocs.org
本文围绕 readthedocs.org 仓库中 subscriptions.rst 所讲解的商业订阅管理功能展开,介绍组织所有者如何在 Read the Docs 商业版中完成信用卡信息更新、套餐升降级、发票查看与下载、取消订阅等操作,并结合仓库源码(readthedocs/subscriptions/、readthedocs/organizations/)揭示其背后的 Stripe 集成原理、订阅生命周期事件处理与组织禁用机制。读完本文,你将掌握订阅管理页面的完整操作流程、月度/年度计费与折扣规则,以及订阅取消后组织会发生什么。
订阅管理:一切从组织所有者开始
Read the Docs 的订阅(subscription)挂在**组织(Organization)**这一层级上,而不是挂在单个用户或单个项目上。也就是说,一个组织拥有一份订阅,组织内所有项目的计费与功能配额都由这份订阅决定。文档明确说明:所有组织所有者(organization owners)都可以管理该组织的订阅。
从源码结构看,这一设计非常清晰:
Organization模型上存在stripe_customer与stripe_subscription两个外键字段,分别指向 Stripe 的客户与订阅对象(见 readthedocs/organizations/models.py);- 订阅相关的视图、表单、任务、事件处理均集中在 readthedocs/subscriptions/ 目录下,与组织的
stripe_id、stripe_customer、stripe_subscription字段直接关联; - 权限控制上,订阅详情视图继承自
OrganizationMixin(见 readthedocs/subscriptions/views.py),只有具备组织访问权限(通常是所有者)的用户才能看到订阅信息。
权限提示(原文档明确强调):查看订阅信息需要组织所有者权限。如果你没有权限,可以请组织现有的所有者代为完成所需的变更操作。此外,Read the Docs 无法通过电子邮件请求取消订阅——电子邮件无法安全地验证用户身份。如果需要通过邮件沟通,你必须先登录 Read the Docs 账号并提交正式的支持请求来完成身份验证。
进入订阅管理页面的路径
原文档给出了两条核心入口:
- 导航到订阅管理页面(组织选择页面中的
subscription_detail路由); - 点击Manage Subscription(管理订阅)按钮。
从路由定义看,subscription_detail与stripe_customer_portal两个 URL 分别绑定DetailSubscription与StripeCustomerPortal视图(见 readthedocs/subscriptions/urls.py)。其中:
subscription_detail(订阅详情页)负责展示当前订阅的主产品、附加产品、功能特性列表,以及订阅的结束日期;stripe_customer_portal(仅接受 POST 请求)负责创建 Stripe Billing Portal 会话,并将用户重定向到 Stripe 托管的账单门户。
点击Manage Subscription后,后端会调用 Stripe 的billing_portal.sessions.create接口,携带当前组织的stripe_customer.id与回跳地址(return_url指向订阅详情页),随后将浏览器 302 跳转到 Stripe 门户 URL(见 readthedocs/subscriptions/views.py)。之后用户便可以在 Stripe 门户中独立完成以下常见操作:
- 更新信用卡信息;
- 升级、降级或取消当前套餐;
- 查看、下载并支付发票;
- 在发票上附加额外的税号(VAT/EIN)或联系邮箱地址。
订阅详情页的构成
DetailSubscription视图(见 readthedocs/subscriptions/views.py)是理解整套逻辑的关键。它在GET请求中渲染订阅详情,并处理两类特殊场景:
- 升级回调:当 URL 携带
?upgraded=true查询参数时,页面会展示“您的套餐已升级!”的成功提示。这个参数是 Stripe Checkout 的回跳标识(源码注释明确说明)。 - 订阅不存在时的自动修复:
get_object方法被lru_cache装饰,内部调用get_or_create_stripe_subscription。如果组织创建时发生异常导致没有订阅对象,用户访问订阅页时会自动重试创建默认订阅(见 readthedocs/subscriptions/utils.py)。
在渲染上下文时,视图会遍历订阅下的所有items,通过RTD_PRODUCTS配置将 Stripe 产品映射为本地RTDProduct对象,区分出主产品(main product)与附加产品(extra products)(例如额外的构建器),并汇总该订阅包含的功能特性列表(见 readthedocs/subscriptions/products.py)。
值得注意的一个细节是欠费(past_due)状态下的结束日期计算:当 Stripe 将订阅标记为past_due时,说明当期费用尚未支付。此时视图会去查找最近一笔已支付发票(或最后一笔未支付发票),用它的period_end作为订阅结束日期展示给用户(见 readthedocs/subscriptions/views.py)。这样用户看到的“结束日期”就是他们实际已付费周期的最后一天,而非未来的名义周期结束日。
取消订阅:账单周期内仍有效,次周期不再续费
原文档对取消订阅给出了明确的行为说明:
取消订阅后,你的订阅将在当前账单周期剩余时间内保持有效,并且不会在下一个账单周期续费。
这意味着取消是一个“软性”动作——你不会立即失去服务,而是服务会在当前付费周期结束时终止。这是 Stripe 订阅模型的典型行为,也与仓库中的事件处理逻辑一致。
从实现角度看,订阅取消由 Stripe 的customer.subscription.deletedwebhook 事件驱动。仓库在 readthedocs/subscriptions/event_handlers.py 中注册了对应的处理器,其行为包括:
- 通知所有所有者:遍历
organization.owners.all(),给每个所有者发送通知。通知类型分两种:- 如果被取消的是试用订阅(即订阅项的价格等于默认试用价格
RTD_ORG_DEFAULT_STRIPE_SUBSCRIPTION_PRICE),发送SubscriptionRequiredNotification(“希望您喜欢 Read the Docs 的试用!”); - 否则发送
SubscriptionEndedNotification(“您的 Read the Docs 订阅已结束”)。
- 如果被取消的是试用订阅(即订阅项的价格等于默认试用价格
- 内部 Slack 告警:如果配置了
SLACK_WEBHOOK_RTD_NOTIFICATIONS_CHANNEL且该客户累计消费金额大于 0,会向内部 Slack 频道推送一条包含组织名、套餐名、消费总额、客户起始时间、项目数、域名数、SSO 认证方式、团队数等信息的消息,用于内部跟进流失客户(见 readthedocs/subscriptions/event_handlers.py)。
订阅取消后组织会发生什么
这是理解“取消订阅”后果的关键。仓库中存在一套禁用(disable)机制:
- 当订阅状态不再是
active或trialing时,customer.subscription.updated/deleted事件处理器会将organization.disabled置为True(见 readthedocs/subscriptions/event_handlers.py); disabled字段的帮助文本写明:“该组织的文档与构建已禁用”(见 readthedocs/organizations/models.py);- 常量
DISABLE_AFTER_DAYS = 30定义了订阅结束后的缓冲天数(见 readthedocs/subscriptions/constants.py),组织禁用并非即时生效,而是给用户留出了续费窗口; - 但存在一个例外:如果组织的
never_disable字段为True(“即使订阅结束也永不禁用该组织”),则跳过禁用逻辑(见 readthedocs/subscriptions/event_handlers.py)。
此外,customer.subscription.created事件处理器会在用户重新订阅时自动把被禁用的组织重新启用(organization.disabled = False),并更新组织指向的最新订阅(见 readthedocs/subscriptions/event_handlers.py)。
试用结束的自动取消
仓库还有一个容易被忽略的细节:试用订阅到期后会被自动取消。在customer.subscription.updated处理器中,如果订阅使用的是默认试用价格且trial_end已过,但状态还不是canceled,系统会主动调用 Stripe 取消该订阅(见 readthedocs/subscriptions/event_handlers.py)。这是因为 Stripe 不会自动结束试用订阅,需要业务侧手动处理。试用期长度由设置RTD_ORG_TRIAL_PERIOD_DAYS = 30控制(见 readthedocs/settings/base.py)。
计费方式:月度与年度订阅
原文档对计费方式的说明非常明确:
- 所有套餐均提供月度与年度两种计费方式;
- 年度套餐相比月度计费享有 2 个月的折扣(即相当于每年只付 10 个月的费用);
- 所有套餐均支持信用卡计费;
- Pro 与 Enterprise 套餐的年度订阅支持发票(invoice-based)与采购订单(PO)计费。
官方建议:Read the Docs 推荐所有用户使用信用卡支付,因为这会极大简化计费流程(原文档 tip 原文)。
从源码看,价格体系来自 Stripe 侧:PlanForm表单会查询get_listed_products()(即RTD_PRODUCTS中listed=True的产品),再过滤出 Stripe 中active=True的Price,按unit_amount升序排列,展示为“产品名(人类可读价格)”的下拉选项(见 readthedocs/subscriptions/forms.py)。也就是说,月度/年度价格、具体金额均由 Stripe 端配置,应用侧只负责展示与传递价格 ID。
产品与功能特性的映射关系定义在RTD_PRODUCTS设置中,每个RTDProduct由以下字段构成(见 readthedocs/subscriptions/products.py):
| 字段 | 含义 |
|---|---|
stripe_id | 对应 Stripe 产品 ID |
features | 该产品包含的功能特性集合(RTDProductFeature) |
listed | 是否允许用户购买(是否在 Plan 表单中展示) |
extra | 是否为可叠加在主套餐之上的附加产品(如额外构建器) |
功能特性类型定义在 readthedocs/subscriptions/constants.py,包括自定义域名(cname)、公共文档 CDN、自定义 SSL、支持 SLA、私有文档、Embed API、搜索分析、页面浏览分析、并发构建数、Google SSO、SAML SSO、自定义 URL、审计日志、页面级审计日志、重定向数量上限等 14 类。当组织同时拥有主套餐与附加产品时,RTDProductFeature.__add__与__mul__运算符会把各订阅项的配额累加(见 readthedocs/subscriptions/products.py),例如两个构建器叠加后并发构建数翻倍。
升级套餐的内部流程
虽然日常的升级/降级在 Stripe Billing Portal 中完成,但仓库也保留了一套基于Stripe Checkout的购买流程(DetailSubscription.post→redirect_to_checkout,见 readthedocs/subscriptions/views.py):
- 用户提交
PlanForm(选择一个plan价格); - 视图校验当前订阅状态,只有已取消(
canceled)的订阅才能发起新购(否则返回 404); - 创建或复用 Stripe 客户(
get_or_create_stripe_customer); - 调用 Stripe Checkout 创建订阅会话,
mode="subscription",payment_method_types=["card"],line_items中放入所选价格; success_url为订阅详情页并追加?upgraded=true,cancel_url为订阅详情页本身;- 重定向到 Checkout 页面完成支付。
支付成功后,Stripe 的customer.subscription.createdwebhook 会触发subscription_created_event,把新订阅挂到组织上并自动重新启用被禁用的组织(见 readthedocs/subscriptions/event_handlers.py)。
折扣与信用:非营利与学术组织享 50% 折扣
原文档在“Discounts and credits”一节明确说明:
- Read the Docs一般不提供软件折扣,但社区托管的广告支持服务(
|org_brand|商业品牌)是例外; - 唯一的标准折扣:经认证的学术与非营利组织,其所有商业套餐均可享受50% 折扣;
- 申请方式:通过 support 页面联系官方支持团队提出申请。
同时文档给出一个实用的选型建议:对于开源项目,社区托管(community hosting)通常是最合适的选择,官方也推荐大多数学术项目使用社区托管;而如果对文档的公开性有约束(例如必须私有托管),那么商业托管(commercial hosting)会更合适。
从代码层面看,“折扣”本身由 Stripe 侧的优惠/价格配置实现,仓库源码中并没有硬编码的折扣逻辑——这与“价格与折扣由 Stripe 端管理”的整体架构一致。因此 50% 折扣的落地方式是:联系支持团队完成认证后,由官方在 Stripe 端为你的组织配置对应的优惠价格或优惠券。
订阅状态机:一张图理解生命周期
综合源码中 readthedocs/subscriptions/event_handlers.py 与 readthedocs/organizations/models.py 的实现,可以梳理出订阅的完整生命周期:
- 组织创建:
get_or_create_stripe_subscription自动为组织创建 Stripe 客户与默认试用订阅(试用期 30 天,价格来自RTD_ORG_DEFAULT_STRIPE_SUBSCRIPTION_PRICE),见 readthedocs/subscriptions/utils.py; - 试用期(trialing):试用到期前,
TrialEndingNotification会在试用创建满 24 天时向所有所有者发送“试用即将结束”的提醒邮件(见 readthedocs/subscriptions/notifications.py 与 readthedocs/subscriptions/tasks.py); - 试用到期未付费:试用订阅被自动取消,发送
SubscriptionRequiredNotification; - 正式订阅(active):正常计费,功能特性按订阅项计算;
- 欠费(past_due)/未支付(unpaid):详情页展示最后一次实际付费的周期结束日;
- 取消(canceled):当前账单周期内继续可用,周期结束后不再续费;
- 禁用(disabled):订阅非活跃状态后组织被标记禁用,文档与构建停止服务;缓冲期 30 天(
DISABLE_AFTER_DAYS),期间OrganizationDisabledNotification每日任务会向所有者发送“组织即将被禁用”的警告邮件(见 readthedocs/subscriptions/tasks.py); - 重新订阅:通过订阅详情页发起 Checkout 购买新订阅,webhook 自动重新启用组织。
Organization.get_stripe_subscription对订阅状态的选取还遵循一个优先级顺序:unpaid→past_due→incomplete_expired→incomplete→active→trialing(见 readthedocs/organizations/models.py),即优先向用户展示最紧急的状态——欠费与未支付排在第一位,因为用户需要先完成支付才能继续使用服务。
常见问题与最佳实践
结合原文档与源码,汇总几个实用要点:
- 谁可以管理订阅?只有组织所有者可以。个人用户或普通成员需要联系组织所有者代为操作。
- 能否通过邮件取消订阅?不能。出于身份验证安全考虑,必须登录账号并通过官方支持渠道提交请求(原文档明确说明)。
- 取消后立刻失效吗?不是。当前账单周期内订阅保持有效,下一周期才停止续费。
- 年度订阅省多少?相比按月支付,年度订阅相当于每年省 2 个月费用(即约 83 折的年付)。
- 哪些计费方式可用?所有套餐支持信用卡;Pro 与 Enterprise 的年度订阅额外支持发票与 PO 计费。
- 学术/非营利组织有优惠吗?经认证后有 50% 标准折扣,通过 support 联系官方申请。
- 订阅到期后文档会怎样?组织会被标记禁用、文档与构建停止服务,但存在 30 天缓冲期且
never_disable组织不受影响;重新订阅后自动恢复。
总结
Read the Docs 的订阅管理是一套以Stripe Billing Portal 为自助操作前台、以 webhook 事件为业务联动后台的完整体系:用户在 Stripe 门户完成信用卡更新、套餐升降级、发票管理;仓库侧则通过customer.subscription.*系列事件自动完成组织启用/禁用、所有者通知、试用到期清理、内部流失预警等工作。理解 readthedocs/subscriptions/ 下的views.py、event_handlers.py、utils.py、products.py与 readthedocs/organizations/models.py 中的disabled、never_disable、stripe_subscription字段,就能完整把握从购买、续费到取消、禁用的订阅全生命周期。
- 后端
- 文档
【免费下载链接】readthedocs.org
The source code that powers readthedocs.org
相关推荐
bottom 温度单位怎么配置成华氏度或开尔文?
bottom 温度单位怎么配置成华氏度或开尔文? bottom 默认以摄氏度(Celsius)显示温度:温度图组件的 y 轴默认以摄氏度为刻度,上限为 100°
后端文档RxJS Subscription 完全指南:订阅生命周期、取消订阅与资源释放机制
RxJS Subscription 完全指南:订阅生命周期、取消订阅与资源释放机制 本指南围绕 rxjs.dev 官方文档 guide/subscription
前端Zephyr 离线开发环境:零门槛一步搭好
Zephyr 离线开发环境:零门槛一步搭好 展会现场只给了一台内网笔记本,Zephyr 固件却必须当晚改完上板——离线开发真能跑通吗?能。照下面步骤走,从源码落
操作系统嵌入式RTOS物联网
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考