fix(storefront): 多价目产品显示价带因子表头(非缓存输入Token/输出tokens/缓存Token)+模型产品去立即购买按钮改「按量付费」字样

- core.py get_customer_price_display 第4步: len(prices)>1 且 label 空/按量/容量时,
  段前拼 factor_label(定价YAML fields 的 label),根治裸「¥2.1 元/百万 | ¥8.4 元/百万」
  客户看不懂各是什么价;单价目(存储)/产线(包月包年)行为不变
- index.ui: is_model(pipeline_llm_model/llm_model) 卡片无 binds(cursor=default,
  不再弹购买框),右下角出「按量付费」文字替代立即购买按钮——purchase_realtime
  只支持 account/storage/pipeline,模型消费走推理入口按量落账,购买按钮本身是坏流程
- README 补「商店展示价与卡片行为」节;验证脚本 test_storefront_model_card.py 30断言全过
This commit is contained in:
yumoqing 2026-09-08 17:56:38 +08:00
parent b0b74a2b9f
commit c74b9cb885
3 changed files with 33 additions and 2 deletions

View File

@ -242,6 +242,19 @@ product_use(product_id, user_id, user_org_id, used_amount, used_unit)
└─ 写入 product_usage_log └─ 写入 product_usage_log
``` ```
### 商店展示价与卡片行为(storefront)
- **展示价唯一入口** `get_customer_price_display(product_id, user_org_id)`:显示价 = 资源真实定价 × 客户折扣率(专属折扣 → `'*'` 兜底 → 1.0;未登录按 `'*'` 报价,访客可见)。
- **多价目必带表头**(2026-09-08):产品有多个价格因子时(如模型按量的 非缓存输入Token/输出tokens/缓存Token 三档),`pricing_text` 每段前拼 `factor_label`(来自定价 YAML fields 的 label),禁止输出裸的「¥2.1 元/百万 | ¥8.4 元/百万」让客户猜含义。单价目(如存储 元/GB月)不加表头;产线用 包月/包年 表头。
- **卡片按钮按 product_type 分流**:
| product_type | 卡片点击 | 右下角 |
|---|---|---|
| pipeline | 进产线 open_tab(规划中→提示) | 订阅制说明 + 立即购买/进入产线 |
| workspace_storage / account | 弹 purchase_confirm 购买框 | 立即购买(未登录→登录后购买) |
| pipeline_llm_model / llm_model | **无点击行为**(cursor=default) | **「按量付费」文字,无按钮** |
模型产品不出购买入口的根因:`purchase_realtime` 只支持 account/storage/pipeline 三类(订阅/买断性质),模型消费走推理入口(会话/llm_v1)按用量落账,无需也无法「购买」。
## 目录结构 ## 目录结构
``` ```

View File

@ -1415,10 +1415,15 @@ class ProductManager:
'discount': discount, 'original_text': ''} 'discount': discount, 'original_text': ''}
# 4. 拼展示文本(折扣≠1 时同时给原价,让客户看见让利/加价幅度) # 4. 拼展示文本(折扣≠1 时同时给原价,让客户看见让利/加价幅度)
# 多价目产品(如模型按量:输入/输出/缓存三档 tokens 价)必须带因子表头,
# 否则客户看到裸的「¥2.1 元/百万 | ¥8.4 元/百万」不知道各是什么价
# (2026-09-08 用户反馈)。factor_label 来自定价 YAML fields 的 label。
parts = [] parts = []
orig_parts = [] orig_parts = []
for p in prices: for p in prices:
head = p['label'] if p['label'] and p['label'] not in ('按量', '容量') else '' head = p['label'] if p['label'] and p['label'] not in ('按量', '容量') else ''
if not head and len(prices) > 1:
head = p.get('factor_label') or p.get('factor') or ''
seg = ('%s ' % head if head else '') + '¥%.4g %s' % ( seg = ('%s ' % head if head else '') + '¥%.4g %s' % (
p['amount'], p['unit_label'] or '元') p['amount'], p['unit_label'] or '元')
parts.append(seg.strip()) parts.append(seg.strip())

View File

@ -56,6 +56,9 @@
{% set pricing_text = (cust or {}).get('pricing_text', '定价暂不可用') if (cust or {}).get('success') else ((cust or {}).get('message') or '定价暂不可用') %} {% set pricing_text = (cust or {}).get('pricing_text', '定价暂不可用') if (cust or {}).get('success') else ((cust or {}).get('message') or '定价暂不可用') %}
{% set original_text = (cust or {}).get('original_text', '') %} {% set original_text = (cust or {}).get('original_text', '') %}
{% set is_pipeline = (p.get('product_type') == 'pipeline') %} {% set is_pipeline = (p.get('product_type') == 'pipeline') %}
{# 模型按量产品:不支持也无需「购买」(purchase_realtime 只支持 account/storage/pipeline),
卡片不弹购买框、不出立即购买按钮,改为「按量付费」字样(2026-09-08 用户要求) #}
{% set is_model = (p.get('product_type') in ('pipeline_llm_model', 'llm_model')) %}
{% set extra = {} %} {% set extra = {} %}
{% if is_pipeline %} {% if is_pipeline %}
{# 产线订阅状态:已订阅→「进入产线」直达;未订阅→「立即购买」弹购买确认 #} {# 产线订阅状态:已订阅→「进入产线」直达;未订阅→「立即购买」弹购买确认 #}
@ -73,7 +76,7 @@
"cheight": 15, "cheight": 15,
"padding": "20px", "padding": "20px",
"bgcolor": "#fff", "bgcolor": "#fff",
"cursor": "pointer", "cursor": {{json.dumps('default' if is_model else 'pointer')}},
"gap": "8px" "gap": "8px"
}, },
{% if is_pipeline %} {% if is_pipeline %}
@ -96,7 +99,7 @@
} }
{% endif %} {% endif %}
], ],
{% else %} {% elif not is_model %}
"binds": [ "binds": [
{ {
"wid": "self", "wid": "self",
@ -221,6 +224,16 @@
} }
] ]
} }
{% elif is_model %}
{
"widgettype": "Text",
"options": {
"text": "按量付费",
"fontSize": "13px",
"color": "#e74c3c",
"fontWeight": "600"
}
}
{% elif userid %} {% elif userid %}
{ {
"widgettype": "Button", "widgettype": "Button",