feat(storefront): 多价目定价改表格展示——表头=定价要素+价格,行=要素组合对应价
病根:qwen-image-3.0-pro 按分辨率×类型四档价,一行文本拼出「按次 ¥0.02 元/张 |
按次 ¥0.02 元/张 | 按次 ¥0.25 元/张 | 按次 ¥0.5 元/张」,客户看不出各价适用条件。
- core.py 新增 _build_price_table:len(prices)>1 时返回 price_table
{headers,rows,html};要素列=计费方式(label有区分度时)+定价维度(filter_labels,
英文维度名中文润饰)+计价因子(factor各行不同时,token三因子);全表同因子(flat)
省略因子列;让利时价格附划线原价;html 全量 escape 防注入
- get_customer_price_display 返回值增 price_table 键;有表格时 original_text 置空
(原价已进表格单元格)
- storefront/index.ui 价格区:有 price_table 渲染 Html 表格,否则原文本路径不变
(单价目无歧义)
- 实测:图像四档/token三因子/视频SR分档/产线包月包年四场景表头行值正确,XSS转义通过
This commit is contained in:
parent
e1963b30c5
commit
100dbe28cd
@ -247,6 +247,7 @@ product_use(product_id, user_id, user_org_id, used_amount, used_unit)
|
||||
- **展示价唯一入口** `get_customer_price_display(product_id, user_org_id)`:显示价 = 资源真实定价 × 客户折扣率(专属折扣 → `'*'` 兜底 → 1.0;未登录按 `'*'` 报价,访客可见)。
|
||||
- **原价仅让利时显示**(2026-09-08):折扣 <1 才输出 `original_text` 原价划线;折扣 ≥1(无折扣/加价,如存储 1.5、产线 1.0)不显示原价——加价时「原价」低于现价还划线是误导。展示页(core.py)与购买页(purchase_confirm.dspy 服务端初始价 + 前端 JS 刷新脚本)三处同规则。
|
||||
- **多价目必带表头**(2026-09-08):产品有多个价格因子时(如模型按量的 非缓存输入Token/输出tokens/缓存Token 三档),`pricing_text` 每段前拼 `factor_label`(来自定价 YAML fields 的 label),禁止输出裸的「¥2.1 元/百万 | ¥8.4 元/百万」让客户猜含义。单价目(如存储 元/GB月)不加表头;产线用 包月/包年 表头。
|
||||
- **多价目表格化**(2026-09-09):`len(prices)>1` 时 `get_customer_price_display` 额外返回 `price_table = {'headers','rows','html'}`——一行文本拼「按次 ¥0.02 | 按次 ¥0.02 | ...」(qwen-image-3.0-pro 按分辨率×类型四档价)客户看不出适用条件。要素列生成规则(`_build_price_table`):计费方式列(label 有区分度时,产线包月/包年)→ 定价维度列(YAML role=filter 的 filter_labels,如 分辨率/类型/SR,英文维度名经 `_DIM_LABEL_POLISH` 润饰为中文)→ 计价因子列(factor 各行不同时,token 三因子)。全表同因子(如 flat=按次)省略因子列——价格单位(元/张)已表达口径。让利(折扣<1)时价格单元格附划线原价。storefront/index.ui 卡片价格区:有 `price_table` 渲染 Html 表格控件,否则走文本+划线原价(单价目无歧义不变)。html 全部经 `html.escape` 转义(维度值来自定价 YAML,防注入)。
|
||||
- **卡片按钮按 product_type 分流**:
|
||||
| product_type | 卡片点击 | 右下角 |
|
||||
|---|---|---|
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import json
|
||||
import time
|
||||
import datetime
|
||||
from html import escape as _html_escape
|
||||
from appPublic.uniqueID import getID
|
||||
import json, time
|
||||
|
||||
@ -1293,6 +1294,100 @@ class ProductManager:
|
||||
return {'success': False, 'message': err}
|
||||
return await fn(ref_id)
|
||||
|
||||
# 多价目表格化:常见英文维度名 → 中文表头润饰(YAML fields 无中文 label 时兜底,
|
||||
# 数据源头 label 由提取层生成,此处只做展示润饰不做语义裁决)
|
||||
_DIM_LABEL_POLISH = {
|
||||
'resolution': '分辨率', 'type': '类型', 'size': '尺寸',
|
||||
'quality': '质量', 'duration': '时长', 'style': '风格',
|
||||
}
|
||||
# ppid_pairs 的通用挂载标签——对表格无区分度,不作为要素列
|
||||
_GENERIC_PRICE_LABELS = ('', '按量', '容量')
|
||||
|
||||
def _build_price_table(self, prices, discount):
|
||||
"""多价目定价表格(2026-09-09 用户要求)。
|
||||
|
||||
病根:同一模型多条定价(qwen-image-3.0-pro 按 分辨率×类型 四档价)拼成
|
||||
一行文本时全是重复前缀「按次 ¥0.02 元/张 | ...」,客户看不出各价的适用
|
||||
条件。改为表格:表头 = 定价要素1..N + 价格,行 = 要素取值组合 → 价格。
|
||||
|
||||
要素列生成规则(回答「什么条件用哪个价」):
|
||||
- 计费方式列:prices[].label 有区分度(非 ''/按量/容量)时出现
|
||||
(产线 包月/包年 两行)
|
||||
- 定价维度列:定价 YAML role=filter 维度(filter_labels),按首次出现序
|
||||
(图像模型 分辨率×类型;视频模型 SR)
|
||||
- 计价因子列:factor 各行不同时出现(token 模型 输入/输出/缓存三因子);
|
||||
全表同因子(如 flat=按次)时省略——价格列单位(元/张)已表达口径
|
||||
|
||||
Returns:
|
||||
{'headers': [...], 'rows': [[要素值..., 价格文本]], 'html': '<table>…'}
|
||||
html 供 .ui 直接嵌入 Html 控件;让利(discount<1)时价格单元格附划线原价。
|
||||
"""
|
||||
cols = [] # [(key, header)]
|
||||
seen = set()
|
||||
|
||||
def _add_col(key, header):
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
cols.append((key, header))
|
||||
|
||||
if any((p.get('label') or '') not in self._GENERIC_PRICE_LABELS
|
||||
for p in prices):
|
||||
_add_col('__label__', '计费方式')
|
||||
for p in prices:
|
||||
for k in (p.get('filter_labels') or {}):
|
||||
header = self._DIM_LABEL_POLISH.get(str(k), '') or str(k)
|
||||
_add_col('f:' + str(k), header)
|
||||
factors = set((p.get('factor') or '') for p in prices)
|
||||
if len(factors) > 1 or not cols:
|
||||
_add_col('__factor__', '计价因子')
|
||||
|
||||
# 表头兜底:无 label 的列按用户口径命名「定价要素N」
|
||||
headers = []
|
||||
for i, (k, h) in enumerate(cols):
|
||||
headers.append(h if str(h).strip() else '定价要素%d' % (i + 1))
|
||||
headers.append('价格')
|
||||
|
||||
rows = []
|
||||
html = ['<table style="border-collapse:collapse;width:100%;'
|
||||
'font-size:11px;line-height:1.5;">']
|
||||
th_style = ('padding:3px 6px;border:1px solid #e2e8f0;background:#f8fafc;'
|
||||
'color:#475569;font-weight:600;text-align:left;'
|
||||
'white-space:nowrap;')
|
||||
td_style = ('padding:3px 6px;border:1px solid #e2e8f0;color:#334155;'
|
||||
'white-space:nowrap;')
|
||||
html.append('<tr>' + ''.join(
|
||||
'<th style="%s">%s</th>' % (th_style, _html_escape(str(h)))
|
||||
for h in headers) + '</tr>')
|
||||
|
||||
for p in prices:
|
||||
vals = []
|
||||
for key, _h in cols:
|
||||
if key == '__label__':
|
||||
v = p.get('label') or ''
|
||||
v = '' if v in self._GENERIC_PRICE_LABELS else v
|
||||
elif key == '__factor__':
|
||||
v = p.get('factor_label') or p.get('factor') or ''
|
||||
else:
|
||||
v = (p.get('filter_labels') or {}).get(key[2:], '')
|
||||
vals.append(str(v if v is not None else ''))
|
||||
price_text = '¥%.4g %s' % (p['amount'], p.get('unit_label') or '元')
|
||||
if discount < 1.0 - 1e-9:
|
||||
price_text += '(原价 ¥%.4g)' % p['original_price']
|
||||
rows.append(vals + [price_text])
|
||||
|
||||
price_html = ('<span style="color:#e74c3c;font-weight:600;">%s</span>'
|
||||
% _html_escape('¥%.4g %s' % (
|
||||
p['amount'], p.get('unit_label') or '元')))
|
||||
if discount < 1.0 - 1e-9:
|
||||
price_html += (' <span style="color:#94a3b8;text-decoration:'
|
||||
'line-through;">¥%.4g</span>' % p['original_price'])
|
||||
html.append('<tr>' + ''.join(
|
||||
'<td style="%s">%s</td>' % (td_style, _html_escape(v))
|
||||
for v in vals)
|
||||
+ '<td style="%s">%s</td></tr>' % (td_style, price_html))
|
||||
html.append('</table>')
|
||||
return {'headers': headers, 'rows': rows, 'html': ''.join(html)}
|
||||
|
||||
async def get_customer_price_display(self, product_id=None, product_code=None,
|
||||
user_org_id=None):
|
||||
"""客户视角展示价 = 资源真实定价 × 客户折扣率(2026-09-06 新增)。
|
||||
@ -1309,8 +1404,10 @@ class ProductManager:
|
||||
未登录(user_org_id 空)时用 '*' 通用折扣报价——展示页对访客可见。
|
||||
|
||||
Returns:
|
||||
{'success', 'prices': [{'label','unit_price','unit_label','amount','unit'}],
|
||||
'pricing_text', 'discount', 'original_text'}
|
||||
{'success', 'prices': [{'label','unit_price','unit_label','amount','unit',
|
||||
'filter_labels','original_price'}],
|
||||
'pricing_text', 'discount', 'original_text',
|
||||
'price_table': {'headers','rows','html'} 或 None(多价目时给出)}
|
||||
"""
|
||||
iface, product, err = await self._get_product_interface(
|
||||
product_id, product_code)
|
||||
@ -1405,6 +1502,7 @@ class ProductManager:
|
||||
'factor_label': pf.get('label', ''),
|
||||
'unit': pf.get('unit', ''),
|
||||
'unit_label': unit_suffix or pf.get('unit_label', ''),
|
||||
'filter_labels': item.get('filter_labels') or {},
|
||||
'original_price': round(total, 6),
|
||||
'amount': round(total * discount, 6),
|
||||
})
|
||||
@ -1414,6 +1512,15 @@ class ProductManager:
|
||||
'prices': [], 'pricing_text': '未配置定价',
|
||||
'discount': discount, 'original_text': ''}
|
||||
|
||||
# 3b. 多价目表格化(2026-09-09 用户要求):同一模型多条定价(如
|
||||
# qwen-image-3.0-pro 按 分辨率×类型 四档价)拼成一行文本时客户
|
||||
# 看不出各价适用条件(「按次 ¥0.02 | 按次 ¥0.02 | ...」),改为
|
||||
# 表格展示:表头 = 定价要素1..N + 价格,每行 = 一组要素取值对应的价格。
|
||||
# 单价目产品仍走文本(无歧义)。
|
||||
price_table = None
|
||||
if len(prices) > 1:
|
||||
price_table = self._build_price_table(prices, discount)
|
||||
|
||||
# 4. 拼展示文本(仅折扣<1 让利时给原价划线,让客户看见优惠幅度;
|
||||
# 折扣≥1(无折扣/加价)不显示原价——2026-09-08 用户定夺:加价时
|
||||
# 「原价」低于现价还划线展示是误导,产线展示页/购买页同规则)
|
||||
@ -1437,9 +1544,13 @@ class ProductManager:
|
||||
if orig_parts:
|
||||
original_text = '原价 %s %s' % (
|
||||
' / '.join(orig_parts), prices[0]['unit_label'] or '元')
|
||||
if price_table:
|
||||
# 有表格时文本降为摘要(一行文本装不下要素条件,仅作降级展示)
|
||||
original_text = ''
|
||||
|
||||
return {'success': True, 'prices': prices, 'pricing_text': pricing_text,
|
||||
'original_text': original_text, 'discount': discount}
|
||||
'original_text': original_text, 'discount': discount,
|
||||
'price_table': price_table}
|
||||
|
||||
async def check_product_availability(self, product_id=None, product_code=None,
|
||||
user_org_id=None):
|
||||
|
||||
@ -55,6 +55,9 @@
|
||||
{% set cust = get_customer_price_display(p.get('id'), user_org_id=userorgid 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', '') %}
|
||||
{# 多价目产品(如模型按分辨率×类型分档)用表格展示定价要素+价格(2026-09-09 用户要求),
|
||||
一行文本拼「A ¥x | A ¥y」客户看不出适用条件;单价目仍走文本 #}
|
||||
{% set price_table = (cust or {}).get('price_table') or none %}
|
||||
{% set is_pipeline = (p.get('product_type') == 'pipeline') %}
|
||||
{# 模型按量产品:不支持也无需「购买」(purchase_realtime 只支持 account/storage/pipeline),
|
||||
卡片不弹购买框、不出立即购买按钮,改为「按量付费」字样(2026-09-08 用户要求) #}
|
||||
@ -157,6 +160,15 @@
|
||||
"fontWeight": "600"
|
||||
}
|
||||
},
|
||||
{% else %}
|
||||
{% if price_table %}
|
||||
{# 多价目:表格展示(要素列+价格列),产线包月/包年、模型分辨率×类型分档都走这里 #}
|
||||
{
|
||||
"widgettype": "Html",
|
||||
"options": {
|
||||
"html": {{json.dumps(price_table.html, ensure_ascii=False)}}
|
||||
}
|
||||
},
|
||||
{% else %}
|
||||
{
|
||||
"widgettype": "Text",
|
||||
@ -177,6 +189,7 @@
|
||||
}
|
||||
},
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if is_pipeline %}
|
||||
{
|
||||
"widgettype": "Text",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user