29 KiB
产品报价系统集成方案
一、背景与目标
将《价格成本核算表兼报价系统》Excel 中的产品数据与报价计算逻辑集成到现有订单管理系统中。
Excel 文件包含 8 个产品线:揉面垫、硅胶烤垫、日用高温布、特氟龙玻纤胶带、特氟龙纯膜胶带、高温布、公斤布、输送带。
核心目标:
- 在系统中完整管理产品数据(多供应商、多成本价、多价格层级)
- 实现公式驱动的报价计算(按㎡、按公斤、输送带加工费等)
- 价格数据由系统维护,替代 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(产品定价规则表)
存储每个产品的计价方式和公式参数。
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 '附加费配置JSON(per_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(供应商产品成本表)
每个产品在每个供应商处的采购成本和型号。
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(产品价格层级表)
每个产品按客户层级的销售价格。
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 表增加字段
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 表增加字段
ALTER TABLE `crm_customer` ADD COLUMN `price_tier` varchar(32) DEFAULT 'default' COMMENT '价格层级编码' AFTER `customer_type`;
四、定价规则引擎设计
4.0 设计原则
目标:新增产品时,只在系统管理界面配置,不需要改代码。
核心思路:报价引擎是一个通用解释器,所有公式逻辑都存储在 product_pricing_rule 的 JSON 配置中。引擎读取配置后,按声明式的结构执行计算,不存在任何产品特定的 if/else 分支。
三条设计约束:
- 公式可配置:
formula_expr字段存储数学表达式,引擎用变量替换求值 - 输入可配置:
pricing_inputs字段定义下单时需要用户填写的字段,前端动态渲染表单 - 附加费可配置:
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 — 输入字段声明
告诉引擎:这个产品下单时需要用户填哪些数据。
[
{
"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 键值对存储:
// 高温布 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 — 附加费配置
附加费项的声明式配置,每项定义名称、计价方式和参数:
{
"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 值:
{
"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 完整产品配置示例
示例 1:0.7mm 硅胶烤垫(面积 + 附加费)
{
"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白公布(重量换算)
{
"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(条件加工费)
{
"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(膜包边×6);0.25mm以上改为+5元/㎡"
}
注意:输送带示例中
processing常量需根据产品厚度动态设置(≤0.25mm 设为 2,>0.25mm 设为 5)。这在管理界面配置时,根据产品规格的厚度值选择填入对应数字即可。如果希望完全自动化,可在 formula_constants 中配置阈值判断,由引擎根据product.thickness自动选择(见 4.3 节扩展机制)。
4.3 引擎核心
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 新增产品的操作步骤(纯配置,不改代码)
- 在「产品管理」中新增产品 → 填写名称、规格、厚度、克重等
- 在「定价规则」中新增:
- 填写
pricing_inputs:在界面勾选/配置该产品需要的输入字段(长度、宽度、接头宽度等) - 填写
formula_expr:输入公式表达式 - 填写
formula_constants:填写公式中的常量值 - 填写
surcharge_json:配置可选加工项(如有) - 填写
formula_note:给业务员看的说明
- 填写
- 在「供应商成本」中录入各供应商的采购价
- 在「价格层级」中录入各层级的销售价
整个过程在管理界面完成,不需要任何人写代码。
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 增加字段
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 → 系统算总额
新流程:
- 业务员选择产品 → 选择规格 → 填写尺寸(长/宽) → 勾选加工项
- 前端调用
/api/quotation/calculate获取成本价和推荐售价 - 业务员确认或调整售价 → 保存为订单明细
- 系统记录完整的计价过程(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 记录
迁移脚本逻辑:
- 创建产品分类:揉面垫、硅胶烤垫、日用高温布、特氟龙玻纤胶带、特氟龙纯膜胶带、高温布、公斤布、输送带
- 解析每个Sheet的规格行,创建 Product 记录(product_name = 产品品类,specification = 规格参数)
- 从"恒宇型号"列提取我司型号
- 从供应商列提取成本价,创建 supplier_product_cost 记录
- 从价格层级列创建 product_price_tier 记录
- 根据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)
- 价格变更历史记录
- 批量导入/更新价格
- 报表:按供应商的成本分析
十、注意事项
- 向后兼容:pricing_type 字段为 nullable,现有产品不受影响,定价类型为 null 时按原有固定单价逻辑处理
- 单位换算:所有计算在后端统一转换为米后计算,前端只做输入
- 精度:金额保留 2 位小数,中间计算保留 4 位
- 权限:供应商成本价仅 admin 和 manager 可见(对 salesman 隐藏),与现有
_filter_order_detail_by_role逻辑保持一致 - 审计:定价规则和价格层级的修改需写入 audit_log