dingdanquanliucheng/docs/产品报价系统集成方案.md
2026-05-26 15:24:41 +08:00

748 lines
29 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.

# 产品报价系统集成方案
## 一、背景与目标
将《价格成本核算表兼报价系统》Excel 中的产品数据与报价计算逻辑集成到现有订单管理系统中。
**Excel 文件包含 8 个产品线**:揉面垫、硅胶烤垫、日用高温布、特氟龙玻纤胶带、特氟龙纯膜胶带、高温布、公斤布、输送带。
**核心目标**
1. 在系统中完整管理产品数据(多供应商、多成本价、多价格层级)
2. 实现公式驱动的报价计算(按㎡、按公斤、输送带加工费等)
3. 价格数据由系统维护,替代 Excel 管理
---
## 二、现有系统差距分析
| 维度 | Excel 中的能力 | 系统现状 | 差距 |
|------|---------------|---------|------|
| 产品定价方式 | 公式驱动(长×宽×单价) | 固定单价 | 缺少维度输入和面积计算 |
| 多供应商成本 | 每个产品有 WW/ZB/BC/AK 等多供应商不同成本价 | 只有一个 cost_price | 缺少 supplier_product_cost 表 |
| 价格层级 | 特批价 / 一级经销 / 二级经销 | 只有一个 sale_price | 缺少 price_tier 管理 |
| 加工费项 | 包边、印刷、logo、钉扣、接头等结构化附加费 | 只有 other_fee_amount | 缺少加工费模板 |
| 计价单位 | 按㎡ / 按公斤(需换算) | 只有 unit 字段 | 缺少 pricing_unit 和换算逻辑 |
---
## 三、数据库设计
### 3.1 新增表
#### product_pricing_rule产品定价规则表
存储每个产品的计价方式和公式参数。
```sql
CREATE TABLE `product_pricing_rule` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` bigint NOT NULL COMMENT '产品ID关联 product 表中该产品的默认规格行)',
`product_name` varchar(128) NOT NULL COMMENT '产品名称冗余',
`pricing_type` varchar(32) NOT NULL DEFAULT 'area' COMMENT '显示类型area 按面积 / kg 按公斤 / unit 按件,用于前端标签展示',
`base_unit_price` decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '基准单价(元/㎡ 或 元/kg',
`pricing_unit` varchar(16) NOT NULL DEFAULT '㎡' COMMENT '计价单位:㎡ / kg / 张 / 米',
`pricing_inputs` text COMMENT '输入字段声明JSON定义下单时需要用户填写的字段',
`formula_expr` text COMMENT '公式表达式,如 round($input.length_m * $input.width_m * $rule.base_unit_price, 2)',
`formula_constants` text COMMENT '公式常量JSON存储阈值、系数等',
`surcharge_json` text COMMENT '附加费配置JSONper_sqm/per_piece/per_linear_m/per_sqm_threshold',
`formula_note` text COMMENT '计算公式说明文本,展示给业务员参考',
`status` tinyint NOT NULL DEFAULT 1,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` tinyint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_pricing_rule_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品定价规则表';
```
#### supplier_product_cost供应商产品成本表
每个产品在每个供应商处的采购成本和型号。
```sql
CREATE TABLE `supplier_product_cost` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` bigint NOT NULL COMMENT '产品ID',
`supplier_id` bigint NOT NULL COMMENT '供应商ID关联 base_supplier',
`supplier_model` varchar(64) DEFAULT NULL COMMENT '供应商型号(如 9011WJ-145g',
`our_model` varchar(64) DEFAULT NULL COMMENT '我司型号(如 9011WJ-145g',
`thickness` varchar(32) DEFAULT NULL COMMENT '厚度(mm)',
`weight_gsm` int DEFAULT NULL COMMENT '克重(g/㎡)',
`base_fabric_weight` int DEFAULT NULL COMMENT '基布克重',
`cost_price` decimal(18,4) NOT NULL COMMENT '该供应商的采购单价',
`cost_unit` varchar(16) NOT NULL DEFAULT '㎡' COMMENT '成本单位:㎡ / kg / 张',
`is_primary` tinyint NOT NULL DEFAULT 0 COMMENT '是否默认/主要供应商',
`remark` varchar(255) DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT 1,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` tinyint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_supplier_product_cost_product_id` (`product_id`),
KEY `idx_supplier_product_cost_supplier_id` (`supplier_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商产品成本表';
```
#### product_price_tier产品价格层级表
每个产品按客户层级的销售价格。
```sql
CREATE TABLE `product_price_tier` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` bigint NOT NULL COMMENT '产品ID',
`tier_code` varchar(32) NOT NULL COMMENT '层级编码special 特批 / tier1 一级经销 / tier2 二级经销 / default 默认',
`tier_name` varchar(64) NOT NULL COMMENT '层级名称',
`price` decimal(18,4) NOT NULL COMMENT '该层级的销售单价',
`price_unit` varchar(16) NOT NULL DEFAULT '㎡' COMMENT '价格单位:㎡ / kg / 张',
`remark` varchar(255) DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT 1,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted` tinyint NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_price_tier` (`product_id`, `tier_code`),
KEY `idx_product_price_tier_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品价格层级表';
```
### 3.2 扩展现有表
#### product 表增加字段
```sql
ALTER TABLE `product` ADD COLUMN `category_id` bigint DEFAULT NULL COMMENT '分类ID' AFTER `category`;
ALTER TABLE `product` ADD COLUMN `pricing_type` varchar(32) DEFAULT NULL COMMENT '计价类型' AFTER `sale_price`;
ALTER TABLE `product` ADD COLUMN `pricing_unit` varchar(16) DEFAULT '㎡' COMMENT '计价单位' AFTER `pricing_type`;
ALTER TABLE `product` ADD COLUMN `thickness` varchar(32) DEFAULT NULL COMMENT '厚度(mm)' AFTER `pricing_unit`;
ALTER TABLE `product` ADD COLUMN `weight_gsm` int DEFAULT NULL COMMENT '克重(g/㎡)' AFTER `thickness`;
ALTER TABLE `product` ADD COLUMN `default_width_m` decimal(10,4) DEFAULT NULL COMMENT '默认宽度(米)' AFTER `weight_gsm`;
ALTER TABLE `product` ADD COLUMN `is_default` tinyint NOT NULL DEFAULT 0 COMMENT '是否默认规格' AFTER `default_width_m`;
```
> 注:`category_id`、`is_default` 已在 ORM model 中定义但 SQL 建表脚本中缺失,此处补齐。
#### crm_customer 表增加字段
```sql
ALTER TABLE `crm_customer` ADD COLUMN `price_tier` varchar(32) DEFAULT 'default' COMMENT '价格层级编码' AFTER `customer_type`;
```
---
## 四、定价规则引擎设计
### 4.0 设计原则
**目标**:新增产品时,只在系统管理界面配置,不需要改代码。
**核心思路**:报价引擎是一个**通用解释器**,所有公式逻辑都存储在 `product_pricing_rule` 的 JSON 配置中。引擎读取配置后,按声明式的结构执行计算,不存在任何产品特定的 if/else 分支。
**三条设计约束**
1. **公式可配置**`formula_expr` 字段存储数学表达式,引擎用变量替换求值
2. **输入可配置**`pricing_inputs` 字段定义下单时需要用户填写的字段,前端动态渲染表单
3. **附加费可配置**`surcharge_json` 字段定义可选加工项,支持条件判断和多种计价方式
### 4.1 配置结构详解
`product_pricing_rule` 表的每个字段承载的职责:
```
pricing_rule
├── pricing_type ← 保留,用于前端显示标签(不再决定计算逻辑)
├── pricing_inputs ← 定义下单表单:需要哪些输入字段
├── formula_expr ← 定义核心计算公式
├── base_unit_price ← 基准单价(被公式引用)
├── formula_constants ← 公式中用到的常量参数
├── surcharge_json ← 定义可选附加费/加工费项
└── formula_note ← 给业务员看的公式说明
```
#### pricing_inputs — 输入字段声明
告诉引擎:这个产品下单时需要用户填哪些数据。
```json
[
{
"key": "length",
"label": "长度",
"type": "number",
"unit_options": ["m", "cm", "mm"],
"default_unit": "m",
"default_value": null
},
{
"key": "width",
"label": "宽度",
"type": "number",
"unit_options": ["m", "cm", "mm"],
"default_unit": "m",
"default_value": null
}
]
```
**type 枚举**`number`(数字)、`select`(下拉选择)、`boolean`(是/否)
前端根据此配置**自动生成输入表单**,无需为每个产品硬编码界面。
不同产品的 pricing_inputs 对比:
| 产品 | pricing_inputs |
|------|---------------|
| 揉面垫/高温布/特氟龙胶带 | length, width |
| 公斤布 | length, width |
| 输送带 | length, width, joint_width, edge_binding_length, is_membrane_edge |
| 硅胶烤垫 | length, width |
> 输送带需要更多输入字段——这就是为什么需要声明式配置,而不是硬编码一个 `dimensions: {length, width}` 的固定结构。
#### formula_expr — 公式表达式
引擎支持的表达式语法:
```
基本运算:+ - * /
变量引用:$input.length_m (前端传入的长度,已转为米)
$input.width_m (前端传入的宽度,已转为米)
$rule.base_unit_price
常量引用:$const.xxx (formula_constants 中的值)
条件表达式:${条件} ? 值A : 值B
函数: round(值, 小数位数)
```
各产品的 formula_expr
**面积计价(揉面垫、高温布、特氟龙胶带等)**
```
round($input.length_m * $input.width_m * $rule.base_unit_price, 2)
```
**公斤布(按重量换算)**
```
round($input.length_m * $input.width_m * $const.price_per_kg / $const.length_per_kg, 2)
```
**输送带(带条件分支)**
```
round(
$input.length_m * $input.width_m * $rule.base_unit_price
+ $input.length_m * $input.width_m * (${input.thickness <= $const.threshold} ? $const.processing_under : $const.processing_above)
+ $input.joint_width * $const.joint_price
+ $input.edge_binding_length * (${input.is_membrane_edge} ? $const.membrane_price : $const.normal_edge_price)
, 2)
```
> 关键点:条件判断 `${thickness <= 0.25} ? 2 : 5` 直接写在公式表达式里,引擎解释执行,不需要代码分支。
#### formula_constants — 公式常量
公式中引用的常量参数,以 JSON 键值对存储:
```json
// 高温布 9018AJ
{
"threshold": null
}
// 输送带
{
"threshold": 0.25,
"processing_under": 2,
"processing_above": 5,
"joint_price": 66,
"normal_edge_price": 4.5,
"membrane_price": 6
}
// 公斤布 0.11白公布
{
"price_per_kg": 58,
"length_per_kg": 4.7,
"weight_gsm": 170
}
```
#### surcharge_json — 附加费配置
附加费项的声明式配置,每项定义名称、计价方式和参数:
```json
{
"edge_binding": {
"name": "包边",
"method": "per_sqm",
"price": 11,
"needs_input": false
},
"large_printing": {
"name": "大印刷",
"method": "per_sqm",
"price": 7,
"needs_input": false
},
"small_logo": {
"name": "小logo",
"method": "per_piece",
"price": 2,
"needs_input": true,
"input_key": "logo_quantity"
},
"button": {
"name": "钉扣",
"method": "per_piece",
"price": 2,
"needs_input": true,
"input_key": "button_count"
}
}
```
**method 枚举与计算规则**
| method | 含义 | 公式 | needs_input |
|--------|------|------|------------|
| `per_sqm` | 按面积 | area_sqm × price | false |
| `per_piece` | 按件 | input_value × price | true |
| `per_linear_m` | 按线性米 | input_value × price | true |
| `per_sqm_threshold` | 条件面积 | area_sqm × (条件 ? price_a : price_b) | false |
附加费也可以用表达式定义 `price_expr`,覆盖固定 price 值:
```json
{
"processing": {
"name": "加工费",
"method": "per_sqm_threshold",
"needs_input": false,
"threshold": {
"field": "thickness",
"lte": 0.25,
"price_true": 2,
"price_false": 5
}
}
}
```
> 当 `method: per_sqm_threshold` 时,引擎检查产品属性或输入参数中的阈值字段,选择不同的单价。
### 4.2 完整产品配置示例
#### 示例 10.7mm 硅胶烤垫(面积 + 附加费)
```json
{
"pricing_type": "area",
"base_unit_price": 39.54,
"pricing_unit": "㎡",
"pricing_inputs": [
{"key": "length", "label": "长度", "type": "number", "unit_options": ["m","cm","mm"], "default_unit": "m"},
{"key": "width", "label": "宽度", "type": "number", "unit_options": ["m","cm","mm"], "default_unit": "m"}
],
"formula_expr": "round($input.length_m * $input.width_m * $rule.base_unit_price, 2)",
"formula_constants": {},
"surcharge_json": {
"edge_binding": {"name": "包边", "method": "per_sqm", "price": 11, "needs_input": false},
"large_printing": {"name": "大印刷", "method": "per_sqm", "price": 7, "needs_input": false},
"small_logo": {"name": "小logo", "method": "per_piece", "price": 2, "needs_input": true, "input_key": "logo_quantity"},
"button": {"name": "钉扣", "method": "per_piece", "price": 2, "needs_input": true, "input_key": "button_count"}
},
"formula_note": "卷料算法:长×宽×单价;有包边:长×宽×(单价+包边价格);印刷款:长×宽×(单价+印刷价格钉扣每张加2元"
}
```
#### 示例 2公斤布 0.11白公布(重量换算)
```json
{
"pricing_type": "area",
"base_unit_price": 10,
"pricing_unit": "㎡",
"pricing_inputs": [
{"key": "length", "label": "长度", "type": "number", "unit_options": ["m","cm","mm"], "default_unit": "m"},
{"key": "width", "label": "宽度", "type": "number", "unit_options": ["m","cm","mm"], "default_unit": "m"}
],
"formula_expr": "round($input.length_m * $input.width_m * $const.price_per_kg / $const.length_per_kg, 2)",
"formula_constants": {
"price_per_kg": 58,
"length_per_kg": 4.7,
"weight_gsm": 170
},
"surcharge_json": {},
"formula_note": "每公斤长度4.7m每公斤价格58元(未税),换算每㎡价=58÷4.7≈12.34"
}
```
#### 示例 3输送带 9008AJ条件加工费
```json
{
"pricing_type": "area",
"base_unit_price": 16,
"pricing_unit": "㎡",
"pricing_inputs": [
{"key": "length", "label": "长度", "type": "number", "unit_options": ["m","cm","mm"], "default_unit": "m"},
{"key": "width", "label": "宽度", "type": "number", "unit_options": ["m","cm","mm"], "default_unit": "m"},
{"key": "joint_width", "label": "接头宽度(m)", "type": "number", "unit_options": ["m"], "default_unit": "m"},
{"key": "edge_binding_length", "label": "包边长度(m)", "type": "number", "unit_options": ["m"], "default_unit": "m"},
{"key": "is_membrane_edge", "label": "是否膜包边", "type": "boolean", "default_value": false}
],
"formula_expr": "round($input.length_m * $input.width_m * $rule.base_unit_price + $input.length_m * $input.width_m * ($const.processing) + $input.joint_width * $const.joint_price + $input.edge_binding_length * (${input.is_membrane_edge} ? $const.membrane_price : $const.normal_edge_price), 2)",
"formula_constants": {
"threshold": 0.25,
"processing_under": 2,
"processing_above": 5,
"processing": 2,
"joint_price": 66,
"normal_edge_price": 4.5,
"membrane_price": 6
},
"surcharge_json": {},
"formula_note": "布带输送带成本:卷料成本+2元/㎡+接头宽度×66+包边长度×4.5膜包边×60.25mm以上改为+5元/㎡"
}
```
> **注意**:输送带示例中 `processing` 常量需根据产品厚度动态设置≤0.25mm 设为 2>0.25mm 设为 5。这在管理界面配置时根据产品规格的厚度值选择填入对应数字即可。如果希望完全自动化可在 formula_constants 中配置阈值判断,由引擎根据 `product.thickness` 自动选择(见 4.3 节扩展机制)。
### 4.3 引擎核心
```python
import re
from decimal import Decimal
class PricingEngine:
"""配置驱动的报价计算引擎"""
def calculate(self, rule, user_inputs, product_attrs=None, supplier_id=None, customer_id=None):
"""
rule: PricingRule ORM 对象
user_inputs: 前端传入的用户输入 {"length": 2, "width": 1.2, "length_unit": "m", ...}
product_attrs: 产品属性 {"thickness": 0.08, ...}(用于阈值判断)
"""
# 1. 单位换算
normalized = self._normalize_inputs(rule, user_inputs)
# 2. 计算基础成本(公式求值)
base_cost = self._evaluate_formula(rule.formula_expr, {
"input": normalized,
"rule": {"base_unit_price": rule.base_unit_price},
"const": rule.formula_constants or {},
"product": product_attrs or {},
})
# 3. 计算附加费
surcharges = self._calculate_surcharges(
rule.surcharge_json, user_inputs, normalized, product_attrs
)
total_surcharge = sum(s["amount"] for s in surcharges)
cost_price = base_cost + total_surcharge
# 4. 查询价格层级
tier_prices = {}
if customer_id:
tier_prices = db.get_price_tiers(rule.product_id, customer_id)
return {
"cost_price": round(cost_price, 2),
"base_cost": round(base_cost, 2),
"surcharge_items": surcharges,
"total_surcharge": round(total_surcharge, 2),
"sale_price_tier": tier_prices,
"formula_detail": self._build_formula_detail(rule, normalized, base_cost, surcharges),
}
def _evaluate_formula(self, expr, context):
"""
公式表达式求值器。
支持四则运算、变量替换、条件表达式、round函数。
安全限制:只允许数学运算,不允许任意代码执行。
"""
resolved = expr
# 替换 $input.xxx / $rule.xxx / $const.xxx / $product.xxx
for prefix, values in context.items():
for key, val in values.items():
resolved = resolved.replace(f"${prefix}.{key}", str(val))
# 替换 ${condition} ? a : b 中的条件
resolved = self._resolve_conditionals(resolved)
# 求值
return float(eval(resolved, {"__builtins__": {}}, {"round": round}))
def _calculate_surcharges(self, surcharge_json, user_inputs, normalized, product_attrs):
"""遍历 surcharge_json 中用户勾选的项,逐项计算"""
results = []
area = normalized.get("length_m", 0) * normalized.get("width_m", 0)
for key, config in (surcharge_json or {}).items():
if not user_inputs.get(f"surcharge_{key}", False):
continue
amount = 0
if config["method"] == "per_sqm":
amount = area * config["price"]
elif config["method"] == "per_piece":
amount = user_inputs.get(config.get("input_key", key), 0) * config["price"]
elif config["method"] == "per_linear_m":
amount = user_inputs.get(config.get("input_key", key), 0) * config["price"]
elif config["method"] == "per_sqm_threshold":
threshold = config.get("threshold", {})
field_val = product_attrs.get(threshold.get("field", ""), 0)
price = threshold["price_true"] if field_val <= threshold.get("lte", 0) else threshold["price_false"]
amount = area * price
results.append({"key": key, "name": config["name"], "method": config["method"],
"amount": round(amount, 2)})
return results
```
**引擎只有约 60 行核心代码**,没有产品特定的 if/else。所有产品差异都由配置驱动。
### 4.4 新增产品的操作步骤(纯配置,不改代码)
1. 在「产品管理」中新增产品 → 填写名称、规格、厚度、克重等
2. 在「定价规则」中新增:
- 填写 `pricing_inputs`:在界面勾选/配置该产品需要的输入字段(长度、宽度、接头宽度等)
- 填写 `formula_expr`:输入公式表达式
- 填写 `formula_constants`:填写公式中的常量值
- 填写 `surcharge_json`:配置可选加工项(如有)
- 填写 `formula_note`:给业务员看的说明
3. 在「供应商成本」中录入各供应商的采购价
4. 在「价格层级」中录入各层级的销售价
**整个过程在管理界面完成,不需要任何人写代码。**
### 4.5 边界情况与安全约束
**公式表达式的安全限制**
引擎的 `_evaluate_formula` 方法只允许数学运算,不允许:
- 任意 Python 代码执行
- 文件/网络访问
- 函数调用(仅预定义的 `round`
实现方式:替换变量后,用 `eval(expr, {"__builtins__": {}}, {"round": round})` 限制执行环境。
**公式复杂度上限**
当前设计覆盖 Excel 中全部已知公式。如果将来出现无法用四则运算+条件表达式描述的逻辑(例如需要循环、查表、调用外部 API说明该产品确实需要代码扩展。此时的扩展方式在引擎中新增一个 `custom_function` 注册机制,允许注册 Python 函数作为公式中的函数调用(如 `$func.special_pricing()`)。但这属于远期扩展,当前不需要实现。
### 4.6 报价计算接口设计
```
POST /api/quotation/calculate
Request:
{
"product_id": 123,
"supplier_id": null,
"customer_id": 456,
"user_inputs": { // 统一输入格式:前端根据 pricing_inputs 动态生成
"length": 2.0,
"width": 1.2,
"length_unit": "m",
"width_unit": "m",
"joint_width": null, // 输送带专用,其他产品为 null
"edge_binding_length": null,
"is_membrane_edge": null
},
"surcharge_selections": { // 勾选的附加费项
"edge_binding": true,
"large_printing": true,
"small_logo": false,
"button": false
},
"surcharge_inputs": { // 附加费的自定义输入值
"logo_quantity": 0,
"button_count": 0
}
}
Response:
{
"product_name": "0.7mm 硅胶烤垫",
"area_sqm": 2.4,
"base_cost": 94.90,
"surcharge_items": [
{"key": "edge_binding", "name": "包边", "method": "per_sqm", "amount": 26.40},
{"key": "large_printing", "name": "大印刷", "method": "per_sqm", "amount": 16.80}
],
"total_surcharge": 43.20,
"cost_price": 138.10,
"sale_price_tier": {
"special": 118.80,
"tier1": 129.60,
"tier2": 144.00
},
"recommended_sale_price": 129.60,
"formula_detail": "2.0m × 1.2m × 39.54 = 94.90 + 包边 26.40 + 印刷 16.80 = 138.10",
"available_surcharge_options": [
{"key": "edge_binding", "name": "包边", "method": "per_sqm", "price": 11, "needs_input": false},
{"key": "large_printing", "name": "大印刷", "method": "per_sqm", "price": 7, "needs_input": false},
{"key": "small_logo", "name": "小logo", "method": "per_piece", "price": 2, "needs_input": true, "input_key": "logo_quantity"},
{"key": "button", "name": "钉扣", "method": "per_piece", "price": 2, "needs_input": true, "input_key": "button_count"}
]
}
```
> 前端通过 `pricing_inputs`(获取输入表单结构)和 `available_surcharge_options`(获取附加费选项)实现完全动态渲染,不硬编码任何产品特定逻辑。
---
## 五、订单明细扩展
### 5.1 sales_order_item 增加字段
```sql
ALTER TABLE `sales_order_item` ADD COLUMN `pricing_type` varchar(32) DEFAULT NULL COMMENT '计价类型快照';
ALTER TABLE `sales_order_item` ADD COLUMN `length_m` decimal(10,4) DEFAULT NULL COMMENT '长度(米)';
ALTER TABLE `sales_order_item` ADD COLUMN `width_m` decimal(10,4) DEFAULT NULL COMMENT '宽度(米)';
ALTER TABLE `sales_order_item` ADD COLUMN `area_sqm` decimal(18,4) DEFAULT NULL COMMENT '面积(㎡)';
ALTER TABLE `sales_order_item` ADD COLUMN `surcharge_detail` text COMMENT '附加费明细JSON';
ALTER TABLE `sales_order_item` ADD COLUMN `processing_detail` text COMMENT '加工费明细JSON';
ALTER TABLE `sales_order_item` ADD COLUMN `supplier_id` bigint DEFAULT NULL COMMENT '供应商ID';
ALTER TABLE `sales_order_item` ADD COLUMN `supplier_model` varchar(64) DEFAULT NULL COMMENT '供应商型号';
ALTER TABLE `sales_order_item` ADD COLUMN `price_tier` varchar(32) DEFAULT NULL COMMENT '价格层级快照';
```
### 5.2 订单创建流程变化
**现有流程**:手动填 sale_price 和 cost_price → 系统算总额
**新流程**
1. 业务员选择产品 → 选择规格 → 填写尺寸(长/宽) → 勾选加工项
2. 前端调用 `/api/quotation/calculate` 获取成本价和推荐售价
3. 业务员确认或调整售价 → 保存为订单明细
4. 系统记录完整的计价过程dimensions、surcharge_detail、pricing_type 等),保证可追溯
---
## 六、后端 API 设计
### 6.1 定价规则管理
```
GET /api/pricing-rules # 列表查询
GET /api/pricing-rules/{product_id} # 获取某产品的定价规则
POST /api/prricing-rules # 新增定价规则
PUT /api/pricing-rules/{id} # 更新定价规则
```
### 6.2 供应商成本管理
```
GET /api/supplier-costs?product_id={id} # 某产品的各供应商成本
POST /api/supplier-costs # 新增供应商成本
PUT /api/supplier-costs/{id} # 更新供应商成本
DELETE /api/supplier-costs/{id} # 删除
```
### 6.3 价格层级管理
```
GET /api/price-tiers?product_id={id} # 某产品的价格层级
POST /api/price-tiers # 新增价格层级
PUT /api/price-tiers/{id} # 更新
DELETE /api/price-tiers/{id} # 删除
```
### 6.4 报价计算
```
POST /api/quotation/calculate # 计算报价(见 4.2 接口设计)
GET /api/quotation/preview/{product_id} # 快速预览(默认参数)
```
### 6.5 产品管理接口扩展
现有 `/api/products` 接口增加:
- 创建/更新产品时,同时写入 `product_pricing_rule`
- 产品列表返回时包含 `pricing_type`、`pricing_unit` 信息
- 产品详情返回时包含供应商成本列表和价格层级
---
## 七、前端改动范围
### 7.1 产品管理页面web-admin
- 产品新增/编辑表单增加:计价类型、计价单位、厚度、克重、默认宽度
- 产品详情页增加:定价规则配置、供应商成本列表、价格层级列表
- 新增「定价规则管理」菜单项
### 7.2 订单创建页面web-sales
- 订单明细行增加:尺寸输入(长/宽 + 单位选择)、附加费勾选
- 选择产品后自动触发报价计算,实时显示成本价和推荐售价
- 支持指定供应商(下拉选择,显示各供应商成本对比)
### 7.3 小程序端mini-manager
- 订单创建同 web-sales 的尺寸输入和自动计算
---
## 八、数据迁移策略
### 8.1 从 Excel 迁移产品数据
Excel 8个Sheet → 对应 8个产品分类 → 每个规格行创建为 Product 记录
**迁移脚本逻辑**
1. 创建产品分类:揉面垫、硅胶烤垫、日用高温布、特氟龙玻纤胶带、特氟龙纯膜胶带、高温布、公斤布、输送带
2. 解析每个Sheet的规格行创建 Product 记录product_name = 产品品类specification = 规格参数)
3. 从"恒宇型号"列提取我司型号
4. 从供应商列提取成本价,创建 supplier_product_cost 记录
5. 从价格层级列创建 product_price_tier 记录
6. 根据Sheet类型创建 product_pricing_rule 记录
### 8.2 迁移数据量估算
| Sheet | 预估产品数 | 预估供应商成本记录 | 预估价格层级记录 |
|-------|-----------|-------------------|----------------|
| 揉面垫 | 2 | 2 | 2 |
| 硅胶烤垫 | 3 | 3 | 3 |
| 日用高温布 | 12 | 12×1~2 | 12 |
| 特氟龙玻纤胶带 | 8 | 8×2~3 | 8×3 |
| 特氟龙纯膜胶带 | 6 | 6×2 | 6×2 |
| 高温布 | 16 | 16×3~5 | 16×3 |
| 公斤布 | 11 | 11×1~3 | 11 |
| 输送带 | 17 | 17×1 | 17×3 |
| **合计** | **~75** | **~200+** | **~250+** |
---
## 九、实现分期建议
### 第一期:数据模型 + 产品管理(预计 2-3 天)
- 新增 3 张数据库表product_pricing_rule、supplier_product_cost、product_price_tier
- product 表补充字段
- crm_customer 增加 price_tier 字段
- 后端 API定价规则、供应商成本、价格层级的 CRUD
- 前端:产品管理页面增加定价规则配置
- 数据迁移脚本:从 Excel 导入产品数据
### 第二期:报价引擎 + 订单集成(预计 2-3 天)
- 后端POST /api/quotation/calculate 报价计算服务
- sales_order_item 增加字段
- 订单创建流程集成报价计算
- 前端:订单创建页面增加尺寸输入和自动报价
- 前端:供应商选择和成本对比
### 第三期:优化(预计 1-2 天)
- 报价单导出PDF
- 价格变更历史记录
- 批量导入/更新价格
- 报表:按供应商的成本分析
---
## 十、注意事项
1. **向后兼容**pricing_type 字段为 nullable现有产品不受影响定价类型为 null 时按原有固定单价逻辑处理
2. **单位换算**:所有计算在后端统一转换为米后计算,前端只做输入
3. **精度**:金额保留 2 位小数,中间计算保留 4 位
4. **权限**:供应商成本价仅 admin 和 manager 可见(对 salesman 隐藏),与现有 `_filter_order_detail_by_role` 逻辑保持一致
5. **审计**:定价规则和价格层级的修改需写入 audit_log