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

29 KiB
Raw Blame History

产品报价系统集成方案

一、背景与目标

将《价格成本核算表兼报价系统》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产品定价规则表

存储每个产品的计价方式和公式参数。

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供应商产品成本表

每个产品在每个供应商处的采购成本和型号。

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_idis_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 分支。

三条设计约束

  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 — 输入字段声明

告诉引擎:这个产品下单时需要用户填哪些数据。

[
  {
    "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 完整产品配置示例

示例 10.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膜包边×60.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 新增产品的操作步骤(纯配置,不改代码)

  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 增加字段

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_typepricing_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