Product Selector 商品选择器组件说明
这个文档只回答两件事:当前 CMS 商品选择器组件有什么、组件上的信息逻辑怎么生成;这次为了支持 one-time payment,需要新增什么能力、展示效果是什么。
1. 目前的商品选择器组件有什么
下面保留现有组件的编号标记、字段来源说明,以及 Monthly / Annual 两种展示效果。这里展示的是订阅商品逻辑,所以会出现月均价、/month、/year、per cam 等订阅文案。
card-long-horizontal
isSelected 控制 radio、边框和背景。visibleProducts[].displayName。formatMonthlyPrice()。getMonthUnit() 输出 / cam / month。getYearUnit() 对应文案。card-long-vertical
offerId 触发顶部渐变区域。visibleProducts[].displayName。formatMonthlyPrice()。/ home / month 等。offerId 非空触发顶部渐变区域。card-square-simple
visibleProducts[].displayName。/mon 与 per cam/per home。card-square-full
visibleProducts[].displayName。formatMonthlyPrice()。formatTotalPrice()。badges[].tipSetting。subhead=15、callout=16、title2=22、title3=20、caption1=12、caption2=11。这是文档级还原,不是 Flutter 截图级像素验收。
2. 此次需要新增的 one-time payment 支持
one-time payment 不应该是一种新的商品选择器组件,也不应该只支持某一个 cardStyle。它应该是 product-container 下的一种商品展示模式:当商品本身的支付类型为 paymentMode = one_time 时,四种现有 cardStyle 都要把订阅文案替换为一次性支付文案。
one-time payment 效果预览
下面四组卡全部基于现有 cardStyle,只替换文案和价格口径。每个 cardStyle 内都枚举 single / unlimited 两种覆盖范围文案;实际实现时,四种 cardStyle 都必须支持 paymentMode = one_time。
card-long-horizontal
card-long-vertical
card-square-simple
card-square-full
基于当前 CMS 后台代码的评估
- 当前已支持:
product-container已有layout、tierType、products[]、visibleProducts[]、defaultProductId。产品可以选择商品 ID、配置展示名称、卡片样式、Offer ID、badge 和默认选中商品。 - 当前只用于后台展示:商品选择器从 marketing-service 查询商品时,前端类型里已经有
month、price、maxDeviceNum。后台列表能看到“周期:1200月”这类信息,但 CMS 最终只保存商品 ID,不保存完整商品事实。 - 当前缺口:CMS 后台、接口 DTO、App 插件模型里都没有
paymentMode;App 侧也没有针对 one-time payment 的文案调用逻辑,现有四种卡片会按订阅逻辑展示月均价和/month。
产品在 CMS 后台需要配置什么
产品侧只配置“展示哪些商品、用哪个样式、标题和 badge 怎么展示”。价格、覆盖设备数、是否 one-time、Cover... / Pay Once 文案都不建议靠 CMS 手填,应该来自商品结构化数据和后端文案库。
| CMS 配置项 | 产品需要填写什么 | 说明 |
|---|---|---|
product-container.layout |
商品选择器整体布局,例如纵向列表。 | 沿用现有 product-container 能力。 |
defaultProductId |
默认选中的商品 ID。 | 例如默认选中 Lifetime 主推商品。 |
products[].productId |
这个 Paywall 涉及的全部商品 ID。 | 当前 CMS 已有。它只保存商品 ID,完整商品信息由后续接口补齐。 |
visibleProducts[].productId |
真正展示在商品选择器里的商品 ID,以及展示顺序。 | 当前 CMS 已有。这里可以同时放 one-time 商品和 subscription 商品,但商品本身是什么支付类型不靠这个字段判断。 |
visibleProducts[].displayName |
商品标题,例如 Lifetime Access、3-Month Pass、Annual。 |
标题仍由 CMS 控制,未来 3 个月、6 个月一次性商品也可以复用。 |
visibleProducts[].cardStyle |
选择卡片样式:card-long-horizontal、card-long-vertical、card-square-simple、card-square-full。 |
四种现有样式都应该支持 one-time 商品。 |
visibleProducts[].badges |
角标文案,例如 POPULAR、BEST VALUE。 |
继续复用现有 badge 配置;不要默认复用订阅年付折扣的 save 计算规则。 |
Cover One Camera、Cover All Cameras、Pay Once 这类文案属于 one-time payment 的通用规则文案,不跟某个 Paywall 强绑定。推荐放在后端文案库 / 多语言资源里,App 按商品数据调用。
诉求 1:CMS 后台要能区分 one-time 和 subscription 商品
paymentMode,枚举值为 subscription / one_time。这个字段应由商品后台或 marketing-service 根据商品真实支付类型生成,再下发给 CMS / App;不要放在 visibleProducts[] 里让产品手填。
| 核验项 | 现有情况 | 结论 |
|---|---|---|
| 20530 喂鸟器一次性商品 | 商品元数据里是 product.month = 1200、tier_id = 209、tier_type = 2、tier_service_type = 0、maxDeviceNum = 1。 |
month = 1200 可以作为历史一次性商品的核验依据。当前 CMS 商品选择器也能拿到 month 用于后台展示,但不建议把 month 当成产品配置项或最终判断字段。 |
| 20404 / 20405 常规订阅商品 | 同样是 tier_type = 2、tier_service_type = 0、maxDeviceNum = 1,但 product.month 分别是 1 / 12。 |
仅靠 tierType、tierServiceType、maxDeviceNum 分不出 one-time 和普通订阅。 |
| 最终给 CMS / App 的字段 | marketing-service 商品查询结果和最终下发给 App 的 allProducts 都带上 paymentMode。 |
CMS 后台展示和筛选用 paymentMode;App 渲染也用 paymentMode 决定展示 one-time 还是 subscription 文案。 |
诉求 2:one-time 文案配置在哪里、怎么调用
one-time 商品会新增 Cover One Camera、Cover All Cameras、Pay Once 这类文案。诉求是:App 首次支持动态文案能力后,后续文案修改不需要 App 发版。
product-container 上新增文案字段。把 one-time payment 的通用文案放到后端文案库 / 多语言资源中;App 看到 paymentMode = one_time 后,根据商品 maxDeviceNum 选择覆盖范围文案,再拼接 Pay Once。后续改文案只更新后端文案库,不需要 App 发版,也不需要改 CMS Paywall 配置。
| 文案 | 产品在 CMS 配什么 | 调用逻辑 |
|---|---|---|
商品标题,例如 Lifetime Access |
visibleProducts[].displayName |
直接展示 CMS 配置的商品标题。 |
Cover One Camera / Cover All Cameras |
不在 CMS 配。 | App 看到 paymentMode = one_time 后,根据商品 maxDeviceNum 从后端文案库选择文案:1 → one camera,2 → two cameras,0 → all cameras。 |
Pay Once |
不在 CMS 配。 | App 看到 paymentMode = one_time 后,从后端文案库读取一次性支付文案并拼接,例如 Cover One Camera · Pay Once。 |
价格,例如 $99.99 |
不在 CMS 手填。 | 仍来自原生支付层真实价格。one-time 模式下展示总价,不展示月均价。 |
POPULAR / BEST VALUE |
visibleProducts[].badges |
直接展示 CMS 配置的 badge。 |
paymentMode 和后端文案库读取;但这次能力上线后,Cover One Camera、Cover All Cameras、Pay Once 的具体文案由后端文案库下发,后续修改文案或翻译不再依赖 App 发版。
CMS 配置示例(产品需要填写)
下面只展示产品在 CMS 后台需要配置的内容:展示哪些商品、默认选中哪个商品、商品标题、卡片样式、badge。one-time 通用文案不在 CMS 配置。
{
"blockType": "product-container",
"layout": "vertical",
"defaultProductId": "one_time_lifetime_single_9999",
"visibleProducts": [
{
"productId": "one_time_lifetime_single_9999",
"displayName": "Lifetime Access",
"cardStyle": "card-long-horizontal",
"badges": [
{
"type": "tip",
"tipSetting": { "tipText": "POPULAR" }
}
]
},
{
"productId": "one_time_lifetime_unlimited_19499",
"displayName": "Lifetime Access",
"cardStyle": "card-long-horizontal",
"badges": [
{
"type": "tip",
"tipSetting": { "tipText": "BEST VALUE" }
}
]
}
]
}
paymentMode、maxDeviceNum、真实价格、Cover... / Pay Once 文案。CMS 可以读取 paymentMode 做展示和校验,但不应该让产品手动填写这些商品事实或规则文案。
后端文案库示例
后端文案库负责维护 one-time payment 的通用多语言文案。App 根据用户语言、paymentMode 和 maxDeviceNum 取对应文案。
{
"locale": "en",
"one_time_payment": {
"coverage_one_camera": "Cover One Camera",
"coverage_two_cameras": "Cover Two Cameras",
"coverage_all_cameras": "Cover All Cameras",
"pay_once": "Pay Once",
"separator": " · "
}
}