
前端CMS【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址https://gitcode.com/gh_mirrors/wp/wp-calypso点击查看免费下载本指南以 wp-calypso 仓库中 client/dashboard/me/billing-purchases/AGENTS.md 为核心系统讲解 Dashboard 客户端中规模最大、复杂度最高的计费购买管理区域其目录结构、数据层约定、Purchase 领域模型、DataViews 列表、取消购买Cancel Purchase三流程分流机制以及 10 条经过线上事故SHILL 系列沉淀的工程陷阱。读者读完将掌握在该模块内新增查询、理解取消/退款流程分流、规避 siteless 购买与 Query Key 前缀等关键坑位的完整方法论。模块定位与目录结构billing-purchases是基于 TanStack Query / TanStack Router 实现的购买Purchase管理模块位于 client/dashboard/me/billing-purchases。它是 Dashboard 客户端中最大也最复杂的计费区域与两个相邻计费体系并存client/me/purchases/——经典Classic计费实现client/my-sites/checkout/——结账Checkout流程。从目录结构可以看出模块的职责划分见 AGENTS.mdbilling-purchases/ ├── index.tsx dataviews.tsx # 购买列表DataViews 表格 ├── purchase-settings/index.tsx # 重量级组件——见架构决策 #2 ├── cancel-purchase/ # 多步骤取消流程最复杂区域 │ ├── cancel-purchase-form/ # 调查问卷步骤、产品专属选项 │ └── domain-removal-flow # 域名专属移除步骤 ├── payment-methods/ # use-create-* 工厂 hooks每种支付方式一个 ├── payment-method-selector/ # 支付方式选择 UI ├── change-payment-method.tsx # 单个购买的支付方式更换 └── add-payment-method.tsx # 独立的新增卡片入口其中cancel-purchase/是模块内最复杂的部分cancel-purchase-form/承载调查问卷步骤与产品专属选项domain-removal-flow承载域名专属移除步骤。purchase-settings/下的子组件如 akismet-api-key-card.tsx、jetpack-license-key-card.tsx、upcoming-renewals-dialog.tsx分别处理各产品类型的专属展示。兄弟计费区域的隐藏陷阱client/dashboard/me/下还有两个相邻计费目录AGENTS.md 明确记录了两处非显而易见的坑目录陷阱billing-payment-methods/删除对话框会查询userPurchasesQuery()以展示受影响的订阅见 payment-method-delete-dialog.tsx其中useQuery( userPurchasesQuery() )正是这一依赖的实现billing-tax-details/当can_user_edit false时为只读——不会报错只是静默禁用表单数据层查询封装与 Query Key 约定购买相关数据由两层封装提供fetcher 层位于automattic/api-corepackages/api-core/src/upgrades负责真实的 HTTP 请求fetchUserPurchases、fetchPurchase、cancelAndRefundPurchase、removePurchase、setPurchaseAutoRenew等。查询层位于automattic/api-queriespackages/api-queries/src/upgrades.ts基于 TanStack Query 的queryOptions/mutationOptions封装 fetcher。新增查询的固定流程先在automattic/api-corepackages/api-core/src/中新增 fetcher再在api-queries中写查询封装。查询不在client/dashboard/data/或client/dashboard/app/queries/中。这是模块的硬性约定。Query Key 前缀upgrades而非purchases这是 AGENTS.md 重点强调的历史性约定代码中可直接验证packages/api-queries/src/upgrades.tsuserPurchasesQuery()→[ upgrades ]userTransferredPurchasesQuery()→[ upgrades, transferred ]sitePurchasesQuery( siteId )→[ upgrades, site, siteId ]purchaseQuery( purchaseId )→[ upgrades, purchaseId ]purchaseCancelFeaturesQuery( ... )→[ upgrades, purchaseId, cancel-features, variant, targetProductSlug ]Receipts 使用receipt前缀见 packages/api-queries/src/me-billing-history.ts 的receiptQueryKey支付方式使用me前缀如[ me, account-recovery ]等模式见 packages/api-queries/src/me-account-recovery.ts。关键警告写错前缀会静默破坏缓存失效silently breaks cache invalidation。例如userPurchasesQuery()的onSuccess会invalidateQueries( userPurchasesQuery() )如果某处用了错误的 key 前缀这一条失效链就断了页面会展示过期数据且无任何报错。路由器加载器预取查询通过路由器加载器router loaders在 client/dashboard/app/router/me.tsx 中加载。例如purchasesIndexRoute预取userPurchasesQuery()、userTransferredPurchasesQuery()、userPaymentMethodsQuery( {} )、allSitesQuery()purchaseSettingsIndexRoute先ensureQueryData( purchaseQuery( parseInt( purchaseId ) ) )再按条件预取站点与存储数据见 me.tsxcancelPurchaseRoute在 loader 中并行加载站点购买、站点功能、产品与计划列表、取消功能清单见 me.tsx。注意parseInt( purchaseId )URL 参数是字符串必须转成数字才能传给查询函数这是陷阱 #9。两种 Purchase 类型绝不混用的领域模型Dashboard 与经典版使用了完全不同的Purchase类型AGENTS.md 对此给出硬性警告永远不要在两者之间拷贝逻辑而不转换字段名和取值。维度DashboardClassic类型来源automattic/api-corepackages/api-core/src/upgrades/types.ts#L76calypso/lib/purchases/types字段风格snake_case如purchase.site_slugcamelCase取值风格expiry_status为auto-renewing、manual-renew等不同的字符串值以expiry_status为例Dashboard 侧完整的取值域定义在 types.tsactive活跃未来到期自动续订开启但续订不临近auto-renewing同active但续订已临近即将到期窗口内manual-renew活跃、未来到期但未开启自动续订、且未到临期——用户必须手动续订expiring活跃、即将到期但未过期、且不会自动续订——需要关注状态expired到期日已过涵盖宽限期subscription_status active与已移除两种情形included随父订阅捆绑如捆绑域名生命周期由父订阅决定one-time-purchase永不过期的一次性购买。配套的语义判断函数集中在 client/dashboard/utils/purchase.ts约 50 个工具函数isExpiring、isExpiredAndInGracePeriod、isRemoved、mightStillAutoRenew、isIncludedWithPlan、isCloseToExpiration等它们是本模块状态机的标准答案来源。架构决策一购买列表使用 DataViews购买列表由 dataviews.tsx 实现基于 WordPresswordpress/dataviews组件体系定义字段fields、过滤器filters、操作actions通过usePersistentView()实现响应式列可见性——WIDE_FIELDS、DESKTOP_FIELDS、MOBILE_FIELDS常量按屏幕宽度控制显示哪些列见 dataviews.tsx#L34-L36默认视图DEFAULT_VIEW指定表格布局、每页 10 条、按站点降序排序、density: balanced见 dataviews.tsx#L38-L56。转移transferred的购买单独拉取userTransferredPurchasesQuery()并在列表中禁用管理操作——所有权转移后的购买不允许原用户操作对应陷阱 #8。架构决策二Purchase Settings 是刻意合并的重量级组件client/dashboard/me/billing-purchases/purchase-settings/index.tsx 是一个 1800 行的单一组件同时处理域名查询domainQuery、siteDifmWebsiteContentQuery存储查询siteMediaStorageQuery相关自动续订状态userPurchaseSetAutoRenewQuery续订对话框upcoming-renewals-dialog.tsx产品专属 key 展示Akismet API Key 卡片、Jetpack License Key 卡片。AGENTS.md 明确说明这是有意的合并intentional consolidation不是拆分候选。新增任何购买详情页级的功能先找这个文件。CancelOrRemoveActionButton也在其中它依据自动续订状态而非is_cancelable来决定展示取消还是移除按钮见陷阱 #1 与 #4。架构决策三支付方式工厂模式payment-methods/目录实现工厂模式每个use-create-*hook 返回一个带 processor 的PaymentMethod对象供支付方式选择器使用use-create-credit-card.tsx——新建信用卡use-create-existing-cards.tsx——已存卡片use-create-paypal-express.tsx/use-create-existing-paypal-ppcp.tsx——PayPal 两类use-create-payment-methods.tsx——汇总入口。use-create-assignable-payment-methods.tsx 负责将这些对象聚合后按allowedPaymentMethodsQuery()过滤加载中allowedPaymentMethods undefined时返回空数组不展示任何支付方式出错时 fail-open展示全部。它的undefined守卫写在最后第 105-107 行AGENTS.md 警告不要在它之前加任何条件逻辑对应陷阱 #2。取消购买流程三种流程类型与 API 映射取消流程的分流逻辑由 getPurchaseCancellationFlowType() 决定。它不接收 props而是从Purchase的三个字段派生is_refundable、hasAmountAvailableToRefund()即refund_amount 0、is_auto_renew_enabled。流程类型定义在 CANCEL_FLOW_TYPE流程类型触发条件对应 API 调用REMOVE已过期、宽限期或不可退款 且 自动续订已关removePurchaseMutation()DELETECANCEL_WITH_REFUND可退款、退款金额 0、自动续订开cancelAndRefundPurchaseMutation()CANCEL_AUTORENEW不可退款、自动续订开关闭自动续订setPurchaseAutoRenew从源码看分流顺序purchase.ts#L840-L862export function getPurchaseCancellationFlowType( purchase: Purchase ): CancelFlowType { const isPlanRefundable purchase.is_refundable; const isPlanAutoRenewing purchase.is_auto_renew_enabled; if ( isPlanRefundable hasAmountAvailableToRefund( purchase ) ) { return CANCEL_FLOW_TYPE.CANCEL_WITH_REFUND; // 可退款 → 立即移除并退款 } if ( isExpiredOrRemoved( purchase ) ) { return CANCEL_FLOW_TYPE.REMOVE; // 已过期且不可退款→ 移除 } if ( ! isPlanRefundable isPlanAutoRenewing ) { return CANCEL_FLOW_TYPE.CANCEL_AUTORENEW; // 不可退款且自动续订开 → 关自动续订 } return CANCEL_FLOW_TYPE.REMOVE; // 不可退款且自动续订已关 → 立即移除 }流程细节调查问卷按产品类型变化Jetpack、域名、套餐、Akismet、Marketplace 各自有不同的步骤cancel-purchase-form/step-components/下的upsell-step.tsx、feedback-step.tsx、jetpack-cancellation-offer-step.tsx等。Agency 合作方购买跳过问卷对应is_partner_managed/is_host_managed标志。Marketplace 套餐取消会级联到站点上的全部 marketplace 订阅。Mutation 的执行时机与缓存守卫use-cancel-mutation-on-confirm.ts 是取消/退款路径的 mutation 封装它揭示了一个关键机制快照购买对象snapshotPurchase因为两个 mutation 都会 invalidateuserPurchasesQuery而它的[upgrades]key 是purchaseQuery的[upgrades, id]的前缀会导致 live purchase 在问卷中途被重新拉取。若不冻结快照is_auto_renew_enabled翻转成 false 会在问卷下重新推导 flowTypeonSurveyComplete就会上报一次从未发生的退款见 第 46-52 行注释。CANCEL_AUTORENEW 只关自动续订购买仍留在用户列表中所以不需要从列表剥离的缓存机制CANCEL_WITH_REFUND 成功后才一次性setQueryData从列表过滤掉该购买第 80-83 行。onSurveyComplete()是流程的收尾点cancel-purchase/index.tsx#L1422-L1457REMOVE提交submitRemovePurchase、CANCEL_AUTORENEW提交submitTurnOffAutoRenew、CANCEL_WITH_REFUND提交submitCancelAndRefundPurchase随后按流程类型决定跳转到移除后的路由还是回到购买设置页展示已取消提示。常见陷阱清单10 条实战避坑指南1. 读取is_partner_managed/is_host_managed不要自行推导合作伙伴预置partner-provisionedJetpack Start的订阅由合作伙伴计费WordPress.com 自助管理不适用。后端直接上报两个字段is_partner_managed合作伙伴预置并计费排除A4A 商店购买is_host_managed由主机商而非代理机构预置的那部分。Agency 通过 WordPress.com 购买可以在这里取消所以只有is_host_managed才阻止 cancel/remove取消流程对它们单独跳过问卷。旧代码用partner_name检查 ! isA4ABillingDragonPurchase()以及[ agency, a4a_agency ]列表来推导新代码应直接用标志位。尚未清扫的旧调用点包括purchase-payment-method.tsx、components/purchase-expiry-status/、经典版manage-purchase/index.tsx。另外is_upgradable、is_cancelable、is_removable、can_explicit_renew在相关场景下服务端都返回 false客户端只有在可见性由其他因素驱动时才需要自己的判断CancelOrRemoveActionButton和经典版renderCancelPurchaseNavItem都依据自动续订状态而非is_cancelable存储附加包没有自己的服务端标志。2. 支付方式列表加载中为空allowedPaymentMethods undefined返回[]不展示任何支付方式出错则 fail-open全部展示。不要在use-create-assignable-payment-methods.tsx的 undefined 守卫之前添加条件逻辑。3.REMOVE和CANCEL是不同 APIgetPurchaseCancellationFlowType对过期购买返回REMOVE映射到 DELETE 调用而不是取消。不要假设取消购买的所有路径都调用同一个 mutation。对应地cancelPurchaseRoute的页面标题也会据此显示 Remove 还是 Cancel见 me.tsx#L448-L469。4. 捆绑购买只能 Removeexpiry_status included的购买随父套餐续订is_auto_renew_enabled对它毫无意义关闭自动续订是 no-op。永远不要为捆绑购买提供 Cancel。只有域名连接domain_map可以单独移除套餐捆绑的其余部分随套餐一起走。参见purchase-settings/index.tsx中的CancelOrRemoveActionButton。5. CRITICALflowType会被静默覆盖在onSurveyComplete()内部state.cancelIntent refund会把CANCEL_AUTORENEW切换成CANCEL_WITH_REFUND。shouldShowRefundEligibilityNotice特性开关也会改变默认路径。源码中的computeEffectiveFlowTypecancel-purchase/index.tsx#L973-L986是这一逻辑的单一事实来源当 URL 带intent时以mutationFlowType为准cancelIntent refund时强制CANCEL_WITH_REFUND在 split cancel/remove 开关下可退款套餐的默认 Cancel 被改为CANCEL_AUTORENEW。6. 问卷完成状态按购买逐个记录问卷完成状态存储在用户偏好user preferences中避免重复问卷调查——一个已经完成过问卷的购买不会再出现新问卷。相关代码可见cancelPurchaseSurveyCompleted()的调用cancel-purchase/index.tsx#L1489-L1491。7. Siteless 购买绝不要触发站点级查询部分产品Akismet、Jetpack、Marketplace使用临时站点siteless.{jetpack|akismet|marketplace.wp|a4a}.com。必须用hasQueryableSite( purchase )utils/purchase.ts#L333-L335包装purchase.is_attached_to_holding_site守卫。不要对这些购买触发任何站点级查询——不只是siteBySlugQuery()还有一切命中/sites/{blog_id}/…的查询siteFeaturesQuery、sitePurchasesQuery、siteByIdQuery、siteDomainsQuery、cancellationOffersQuery、备份查询等。展示信息用purchase.domain或purchase.blog_id并直接跳过依赖站点的 UI。背后的深层原因SHILL-2295用户不是 holding site 的成员这些请求返回403 authorization_required。AuthProviderapp/auth/index.tsx订阅整个查询缓存曾把 403 当作登出会话处理并重定向到/log-in随即反弹回来形成死循环。现在它的分类器要求statusCode 401才重定向403 不再触发跳转但仍应禁用这些查询——错误会落进缓存所有下游消费者都得应付它。hasQueryableSite()是 holding-site 检查不是可达性检查。购买也可能指向一个所有者已被移除的真实站点Jetpack 断开连接时 WPCOM 会把用户从博客移除已删除站点行为相同。此时blog_id存在、is_attached_to_holding_site为 falsehasQueryableSite()返回true但/upgrades?site{blog_id}等接口返回403 unauthorized/User cannot access upgrades.。目前没有任何购买字段上报这种情况因此路由加载器必须对站点级ensureQueryData调用.catch()——未处理的 rejection 会让整个路由失败进入通用500 Error页用户根本无法进入流程SHILL-1442。cancelPurchaseRoute和purchaseSettingsIndexRouteapp/router/me.tsx都做了这件事。最后用enabled守卫查询时加载条件要读isLoading而不是isPending——被禁用的查询永远保持isPending基于isPending的守卫会让界面永远停在加载占位符上。8. 转移transferred购买先查所有权再允许操作列表中的转移购买由userTransferredPurchasesQuery()单独拉取其管理操作被禁用isTransferredOwnership判断见 utils/purchase.ts#L285-L292。9. 路由参数是字符串URL 参数中的purchaseId必须parseInt()后再传给查询函数——purchaseQuery( parseInt( purchaseId ) )是所有相关 route loader 的统一写法。10.site.options.unmapped_url对.home.blog站点不可信即使站点的免费域名是.home.blog或其他.blog子域名它返回的也是.wordpress.comURL。不要用它渲染用户免费主机名。应读取siteDomainsQuery( siteId )中的真实 WPCOM 域名——找到带wpcom_domain或is_wpcom_staging_domain标志的条目用它的domain字段。这与经典版site.wpcom_url即withoutHttp( unmapped_url )同根同源参见 client/me/purchases/AGENTS.md 陷阱 #6。小结billing-purchases是 wp-calypso Dashboard 中一套高度工程化的计费模块TanStack Query 管理数据与缓存以upgrades前缀为轴心、TanStack Router 管理路由与预取、DataViews 驱动响应式购买列表、工厂模式封装支付方式、三流程分流驱动取消/退款/移除。理解其数据层约定与 10 条陷阱是在该模块安全新增功能、排查线上问题尤其是 siteless 购买与缓存失效类问题的前提。赞分享前端CMS【免费下载链接】wp-calypsoThe JavaScript and API powered WordPress.com项目地址https://gitcode.com/gh_mirrors/wp/wp-calypso点击查看免费下载相关推荐wp-calypso Classic Purchases 模块技术指南Redux 架构下的购买与账单全生命周期管理wp calypso Classic Purchases 模块技术指南Redux 架构下的购买与账单全生命周期管理 本文基于 wp calypso 仓库 cl前端CMSWordPress.com Calypso 深度解析Classic Purchases 模块的账单与购买管理架构WordPress.com Calypso 深度解析Classic Purchases 模块的账单与购买管理架构 本文以 CalypsoWordPress.前端CMSwp-calypso 购买页埋点TrackPurchasePageView 组件的工作原理与实现详解wp calypso 购买页埋点TrackPurchasePageView 组件的工作原理与实现详解 本文围绕 wp calypso 中 client/me/前端CMS上一篇如何选对.NET Framework版本dotnet官方文档对比4.5到4.8.1共11个版本含支持状态清单下一篇wepy/use-promisify让 WePY 小程序 API 全面 Promise 化创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考