From 100dbe28cdd45106be6962379e4c15f84205dff0 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 9 Sep 2026 11:59:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(storefront):=20=E5=A4=9A=E4=BB=B7=E7=9B=AE?= =?UTF-8?q?=E5=AE=9A=E4=BB=B7=E6=94=B9=E8=A1=A8=E6=A0=BC=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E2=80=94=E2=80=94=E8=A1=A8=E5=A4=B4=3D=E5=AE=9A=E4=BB=B7?= =?UTF-8?q?=E8=A6=81=E7=B4=A0+=E4=BB=B7=E6=A0=BC,=E8=A1=8C=3D=E8=A6=81?= =?UTF-8?q?=E7=B4=A0=E7=BB=84=E5=90=88=E5=AF=B9=E5=BA=94=E4=BB=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 病根: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转义通过 --- README.md | 1 + product_management/core.py | 117 +++++++++++++++++++++++++++++++++++- wwwroot/storefront/index.ui | 13 ++++ 3 files changed, 128 insertions(+), 3 deletions(-) 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( + '' % (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( + '' % (td_style, _html_escape(v)) + for v in vals) + + '' % (td_style, price_html)) + html.append('
%s
%s%s
') + 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",