diff --git a/README.md b/README.md
index 47f7c5b..507fc15 100644
--- a/README.md
+++ b/README.md
@@ -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 | 卡片点击 | 右下角 |
|---|---|---|
diff --git a/product_management/core.py b/product_management/core.py
index 4c46e8b..7313a22 100644
--- a/product_management/core.py
+++ b/product_management/core.py
@@ -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': '
…'}
+ 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 = ['']
+ 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('' + ''.join(
+ '| %s | ' % (th_style, _html_escape(str(h)))
+ for h in headers) + '
')
+
+ 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 = ('%s'
+ % _html_escape('¥%.4g %s' % (
+ p['amount'], p.get('unit_label') or '元')))
+ if discount < 1.0 - 1e-9:
+ price_html += (' ¥%.4g' % p['original_price'])
+ html.append('' + ''.join(
+ '| %s | ' % (td_style, _html_escape(v))
+ for v in vals)
+ + '%s |
' % (td_style, price_html))
+ html.append('
')
+ 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):
diff --git a/wwwroot/storefront/index.ui b/wwwroot/storefront/index.ui
index b0c1367..fa4e05f 100644
--- a/wwwroot/storefront/index.ui
+++ b/wwwroot/storefront/index.ui
@@ -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",