product_management/README.md
yumoqing 100dbe28cd 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转义通过
2026-09-09 11:59:14 +08:00

294 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# product_management
产品管理模块 —— 统一管理产品目录、类别树、资源绑定、订阅、消费引擎及资源模块对接。
## 模块定位
product_management 是产品生命周期的中枢,负责:
1. 管理产品类别树和产品注册表
2. 从资源模块(如 llmage导入类别和产品
3. 通过标准接口调度资源模块完成展示、预检、执行、计费
4. 管理资源绑定、供应商路由、订阅与配额
5. 记录消费日志并生成统计
## 数据模型
| 表名 | 说明 |
|------|------|
| `product_category` | 产品类别树parent_id 层级关系resource_module 标记来源模块) |
| `product` | 产品注册表resource_ref_id 关联资源模块内部ID |
| `product_resource` | 产品-资源绑定(一个产品可绑定多个资源,含配额和溢出产品) |
| `product_resource_supplier` | 资源绑定的供应商路由(优先级+权重) |
| `product_subscription` | 产品订阅(月度/按量,含配额跟踪和溢出计费) |
| `product_usage_log` | 消费日志(成本、售价、供应商、计费模式) |
### 关键字段
**product_category**
- `resource_module` — 资源模块名称(如 `llmage`),标识该类别下的产品来自哪个模块
- `has_product` — 是否可挂产品(`1`=是,`0`=仅分类节点)
- `product_type` — 产品类型标识(传递给产品的 product_type
**product**
- `resource_ref_id` — 资源模块内部 ID如 llmage 中 `llm.id`),用于直接调用资源模块接口,无需维护 product_code 对照表
- `product_code` — 产品编码(业务可读标识)
- `product_type` — 产品类型(如 `llm_model`, `llm_monthly`, `compute`
## 架构设计
### 1. 资源模块解耦导入
资源模块(如 llmage和产品模块product_management职责分离
```
资源模块 产品模块
───────── ─────────
load_product_category_product() import_categories_and_products()
│ 只读自己的数据 │ 调用资源模块获取标准化数据
│ 返回标准化格式 │ 负责所有写入和去重逻辑
└──────────────────┐ │
▼ │
{categories, products}
│ │
└───────────►│
│ 类别: name+parent_id+org_id 去重 → 已存在则跳过
│ 产品: resource_ref_id+org_id 去重 → 已存在则更新
```
**资源模块返回格式:**
```python
{
'success': True,
'categories': [
{
'source_id': '资源模块内部类别ID',
'name': '类别名称',
'description': '描述',
'product_type': 'llm_model',
'product_type_title': '大模型API',
'sort_order': 0,
},
],
'products': [
{
'source_category_id': '对应的 source_id',
'resource_ref_id': '资源模块内部ID如 llm.id',
'product_code': '产品编码',
'product_name': '产品名称',
'product_type': 'llm_model',
'brief_intro': '简介',
'sort_order': 0,
},
],
}
```
**去重规则:**
- 类别:按 `name + parent_id + org_id` 判断,已存在则跳过
- 产品:按 `resource_ref_id + org_id` 判断,已存在则更新(名称、简介等),不存在则创建
### 2. 资源模块接口调度
产品模块通过 `product_interface` 字典调度资源模块,所有接口参数统一使用 `resource_ref_id`
```
调用方UI/API
│ env.get_product_display_info(product_id='xxx')
product_management (调度层)
│ 1. 查 product 表 → 获取 resource_ref_id
│ 2. 查 product_category 表 → 获取 resource_module
│ 3. 从 ServerEnv.product_interface 获取接口实现
│ 4. 调用接口函数,传入 resource_ref_id
资源模块(如 llmage/product_interface.py
│ 直接用 resource_ref_id 操作自己的数据
└─ 返回结果
```
### 3. ServerEnv 注册
模块初始化时将所有方法注册到 ServerEnv
```python
def load_product_management():
env = ServerEnv()
# 基础 CRUD
env.get_product_brief = get_product_brief
env.get_product_detail = get_product_detail
env.purchase_product = purchase_product
env.use_product = use_product
env.get_category_tree = get_category_tree
env.get_products_by_category = get_products_by_category
env.import_categories_and_products = import_categories_and_products
# 资源模块接口调度
env.get_product_display_info = get_product_display_info
env.check_product_availability = check_product_availability
env.check_product_consumable = check_product_consumable
env.execute_product = execute_product
env.execute_product_stream = execute_product_stream
env.calculate_product_cost = calculate_product_cost
```
## 资源模块标准接口规范
任何资源模块(如 llmage、supplychain要对接产品模块需实现以下 7 个接口函数并在 `load` 时注册到 `ServerEnv.product_interface`
### 接口函数列表
| # | 函数名 | 参数 | 返回值 | 用途 |
|---|--------|------|--------|------|
| 1 | `get_product_display` | `(resource_ref_id)` | `{success, pricing_text, pricing_detail, extra_info}` | 展示定价信息 |
| 2 | `check_product_availability` | `(resource_ref_id, user_org_id)` | `{available, reason}` | 检查可用性 |
| 3 | `check_product_consumable` | `(resource_ref_id, user_id, user_org_id)` | `{consumable, reason, min_balance, pricing_available}` | 消费前综合预检 |
| 4 | `execute_product_service` | `(resource_ref_id, user_id, user_org_id, request_data)` | `{success, result, usage_data, task_id, status}` | 执行服务(非流式) |
| 5 | `execute_product_service_stream` | `(resource_ref_id, user_id, user_org_id, request_data)` | `AsyncGenerator[{chunk, usage_data, done}]` | 执行服务(流式) |
| 6 | `calculate_product_cost` | `(resource_ref_id, usage_data, user_org_id)` | `{success, amount, cost, discount, pricing_program_id}` | 计算费用 |
| 7 | `load_product_category_product` | `(parent_category_id)` | `{success, categories, products}` | 导入数据(标准化格式) |
### 注册方式
资源模块在其 `__init__.py``init.py``load_*` 函数中注册:
```python
def load_llmage():
env = ServerEnv()
# ... 其他初始化 ...
from .product_interface import (
get_product_display, check_product_availability,
check_product_consumable, execute_product_service,
execute_product_service_stream, calculate_product_cost,
)
env.product_interface = {
'module_name': 'llmage',
'get_product_display': get_product_display,
'check_product_availability': check_product_availability,
'check_product_consumable': check_product_consumable,
'execute_product_service': execute_product_service,
'execute_product_service_stream': execute_product_service_stream,
'calculate_product_cost': calculate_product_cost,
}
```
### resource_ref_id 映射
- `resource_ref_id` 存储在 `product.resource_ref_id` 字段
- 对于 llmage`resource_ref_id = llm.id`
- 产品模块调度时自动从 product 表提取该字段,资源模块直接使用
- 资源模块无需知道 product_code产品模块无需知道资源模块内部结构
## 核心业务流程
### 产品导入
```
UI 操作 → import_category_products.dspy
→ env.import_categories_and_products('llmage', org_id, parent_cat_id, user_id)
→ importlib.import_module('llmage.init')
→ llmage.load_product_category_product(parent_cat_id) # 返回标准化数据
→ 产品模块执行去重写入
→ 返回统计: 新增N类别, 跳过M类别, 新增X产品, 更新Y产品
```
### 消费流程
```
1. 展示阶段
env.get_product_display_info(product_id)
→ 查 product → 查 category → 获取 resource_module
→ 调用 iface.get_product_display(resource_ref_id)
→ 返回定价文本 + 详情
2. 预检阶段
env.check_product_consumable(product_id, user_id, user_org_id)
→ 调用 iface.check_product_consumable(resource_ref_id, user_id, user_org_id)
→ 检查: 模型状态 + ppid定价 + 余额 ≥ min_balance
→ 返回: consumable + reason
3. 执行阶段
env.execute_product(product_id, request_data, user_id, user_org_id)
→ 调用 iface.execute_product_service(resource_ref_id, user_id, user_org_id, request_data)
→ 根据 llm.stream 选择 sync/async/stream 路径
→ 返回: result + usage_data + task_id
4. 计费阶段
env.calculate_product_cost(product_id, usage_data, user_org_id)
→ 调用 iface.calculate_product_cost(resource_ref_id, usage_data, user_org_id)
→ 调用 pricing 模块计算 + 客户折扣
→ 返回: amount + cost + discount
```
### 消费引擎 (product_use)
核心消费路由逻辑,支持订阅制和按量制:
```
product_use(product_id, user_id, user_org_id, used_amount, used_unit)
├─ 月度产品 (llm_monthly):
│ ├─ 查有效订阅 → 配额内扣减 (billing_mode='1')
│ └─ 超出配额 → 溢出计费 (overflow_rate × 超出量)
├─ 按量产品:
│ └─ sell_price = used_amount × product.price
├─ 供应商路由:
│ └─ product_resource → product_resource_supplier (按 priority+weight)
├─ 成本计算:
│ └─ supplychain.supplier_resource_price → unit_cost × used_amount
└─ 写入 product_usage_log
```
### 商店展示价与卡片行为storefront
- **展示价唯一入口** `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 | 卡片点击 | 右下角 |
|---|---|---|
| pipeline | 进产线 open_tab规划中提示 | 订阅制说明 + 立即购买/进入产线 |
| workspace_storage / account | purchase_confirm 购买框 | 立即购买未登录登录后购买 |
| pipeline_llm_model / llm_model | **无点击行为**cursor=default | **按量付费文字无按钮** |
模型产品不出购买入口的根因`purchase_realtime` 只支持 account/storage/pipeline 三类订阅/买断性质模型消费走推理入口会话/llm_v1按用量落账无需也无法购买」。
## 目录结构
```
product_management/
├── models/ # 数据模型定义 (JSON)
│ ├── product.json
│ ├── product_category.json
│ ├── product_resource.json
│ ├── product_resource_supplier.json
│ ├── product_subscription.json
│ └── product_usage_log.json
├── product_management/
│ ├── __init__.py # ServerEnv 注册 + 入口函数
│ └── core.py # ProductManager 全部业务逻辑
├── init/
│ └── data.yaml # 初始化数据 (appcodes 编码等)
├── json/ # CRUD 定义 (由 xls2crud 生成)
├── wwwroot/ # 前端页面 (由 xls2ui 生成)
├── README.md
└── setup.py
```
## 新增资源模块对接指南
以新模块 `supplychain` 为例
1. **实现接口函数** `supplychain/product_interface.py` 中实现上述 7 个函数
2. **注册到 ServerEnv** `supplychain/init.py` `load_supplychain()` 中注册 `product_interface` 字典
3. **导入函数** 实现 `load_product_category_product(parent_category_id)` 返回标准化格式
4. **配置 appcodes** `init/data.yaml` `resource_module` 下添加 `supplychain` 键值对
5. **创建类别** UI 中创建产品类别`resource_module` 选择 `supplychain`
6. **触发导入** 点击"产品导入"按钮自动调用导入流程
所有参数使用 `resource_ref_id`supplychain 的内部 ID产品模块自动处理映射