给一家不锈钢装饰加工厂写核价系统的过程中,我把"一套门套到底多少钱"这件事拆成了七条口径。每一条都踩过坑,每一条都不是看两眼就能想到的。
文中所有数字来自实际跑通的代码,价表用占位值。不是软文,最后我会说清楚这套东西值多少钱取决于什么。
引子:1.89 ㎡ 和 3.40 ㎡
一个门洞,900 宽、2100 高。
老板接电话时脑子里过的是:0.9 × 2.1 = 1.89 平米。按 304 拉丝一平米多少钱,乘一下,报出去。
但车间真正要下的料,是 3.40 平米。
差了 80%。
这 80% 从哪来的?不是"损耗"两个字能解释的。它来自三件具体的事:
- 套线有宽度。门套不是一张平板贴在洞口上,它有正面可见的面宽(比如 60mm),还有折到墙里去的那一段。
- 墙有厚度。240 的墙,意味着每道折边要往里折 240mm。这 240mm 是要花钱买板的。
- 双包是两面。墙的两面各包一次。
把这三件事摊开,一条料就不再是"门洞的一条边",而是一条宽 319.5mm、长 2175mm 的板。
ini
摊平宽度 Wd = 套线面宽 c + 折边道数 k × 抱墙深度 a + 安装折脚 − 折弯扣减
= 60 + 1 × 240 + 20 − 0.5
= 319.5 mm
立边长 Lv = 洞口高 H + 套线面宽 c + 端头余量 = 2100 + 60 + 15 = 2175 mm
横边长 Lh = 洞口宽 W + 套线面宽 c + 端头余量 = 900 + 60 + 15 = 975 mm
单面净展开面积 = (2 × 2175 + 975) × 319.5 / 1e6 = 1.701 ㎡
双包 = 1.701 × 2 = 3.40 ㎡
到这里,第一条口径就出来了。
口径一:计价面积是"摊平展开面积",不是洞口投影
这是所有外行做报价系统的第一个坑,也是最贵的一个。
投影面积(W × H)只能用来对外说"我们按洞口面积计价"这种营销口径。内部算成本必须走展开,因为你要买的是板,不是洞。
两者差多少?实测样例:
| 门洞 | 投影面积 | 净展开(双包) | 倍数 |
|---|---|---|---|
| 900 × 2100,墙厚 240 | 1.89 ㎡ | 3.40 ㎡ | 1.80× |
如果按投影算材料,用料面积会从 3.67 ㎡ 变成 2.04 ㎡,少算四成多。而在这个行业里,材料费占总成本的 72%(后面会讲为什么这个数字重要)------总成本会被低估三成左右,报出去的价基本等于白干。
有一个细节容易漏:折弯那道扣减。
折弯要扣中性轴
板子折 90°,折弯处外侧拉伸、内侧压缩,中性轴不变。所以每道折边实际少用半个板厚:
折弯扣减 = 折边道数 k × 0.5 × 板厚 t
1.0 的板每道扣 0.5mm,1.2 的板每道扣 0.6mm。
听起来微不足道。但一条料的摊平宽度也就 320mm 左右,扣 0.5mm 是 0.16%;而如果忘了扣 ,师傅照着这个宽度下料,折完就短了------整条料报废。
这类"数值上很小、工艺上致命"的参数,是行业软件和通用软件最不一样的地方。
口径二:墙厚 D 不是可选参数,是价格主因
大部分报价模板里,"墙厚"是个备注字段。
在这个模型里,它是主输入。因为墙厚直接决定抱墙深度,而抱墙深度直接进出摊平宽度:
ini
双包:抱墙深度 a = D(一直折到对面)
单包:抱墙深度 a = min(D, 抱墙上限)(单包有上限,超了要配收边条)
240 墙和 120 墙,同一尺寸的门套,展开面积差多少?
- D=240:Wd = 60 + 240 + 20 − 0.5 = 319.5mm → 3.40 ㎡
- D=120:Wd = 60 + 120 + 20 − 0.5 = 199.5mm → 2.12 ㎡
面积差 38%,报价能差一倍以上(因为还叠加了折弯、包装、物流的联动)。而客户在电话里说的往往是"就普通门洞"------你必须追问墙厚。
同一个输入参数,在报价系统里的地位是分三六九等的。墙厚属于"不问清楚就没法报价"的那一类。
口径三:成本加成率 30%,实际利润率只有 23.1%
这条不涉及几何,但它是老板最容易算错的一条,而且直接影响他信不信你的系统。
ini
报价 = 成本 × 1.30 ← 这是"成本加成 30%"
利润率 = (报价 − 成本) / 报价 = 0.30 / 1.30 = 23.1% ← 这才是"销售利润率 23.1%"
他嘴上说"我要 30 个点利润",心里想的和做出来的是两件事。
所以模型里我留了两个开关,二选一:
python
if margin_rate is not None: # 他说"我要 30% 利润"
quote = cost / (1 - margin_rate)
else: # 他说"成本加三成"
quote = cost * (1 + markup_rate)
这两个口径不能混用,也不能悄悄帮他换算------必须让他自己选,界面上写清楚。因为一旦他按 1.3 报出去、回头发现只有 23 个点,他会觉得是软件算错了。
报价系统里,口径歧义比计算错误更伤信任。
口径四:造型改的是几何长度,不只是损耗率
最初我以为"造型"就是给个损耗系数:直边 8%、上圆角 12%、半圆拱 15%。
做完几何展开才发现,造型改变的是件清单本身:
scss
上圆角 R:
立边 Lv −= R
横边 Lh −= 2R
新增 2 段 1/4 圆弧,弧长 = π/2 × R
→ 中心线净变化 = R × (π/2 − 2) = −0.4292 R / 角
注意最后那个负号。
上圆角在面积上是省料的(每角少 0.4292R)。一个 R80 的角,中心线短 34mm。
那为什么加圆角的报价反而更贵?因为贵的地方根本不在板:
- 滚弯工时(按米计,比直边折弯贵几倍)
- 12% 而不是 8% 的损耗(滚弯整形容易废)
- 镜面/镀钛滚弯后要返工抛光,表面费上浮 15%
只算面积、不算工时的人,会把带圆角的活报便宜,然后做完发现不赚钱。
半圆拱:半径不是那个半径
这条我们出过 bug,值得单独说。
半圆拱的真实拱半径是:
ini
R = (洞口宽 W + 横边加长) / 2
而不是参数表里那个 arc_radius。
arc_radius 是"上圆角"造型用的圆角半径(默认 60),半圆拱根本不用它。但两个字段都叫"半径",UI 一排就挨着,结果就是------师傅照着 R60 去滚,实际要 R480。
拱的半径错了,弧长就错了,展开面积、滚弯米数、下料长度全错。这是一个字段名污染了一整条计算链。
修法很土:把两个参数在数据结构上彻底分开,arc_radius 只在上圆角分支里可读,半圆拱分支连它的名字都取不到。
口径五:物流必须"泡货取大",而且顺序不能反
不锈钢重,但门套是空心的、还长。这类货叫泡货------按重量算运费你会亏,按体积算才合理。
规则是三条取最大值:
scss
物流费 = max(体积 × 体积单价, 重量 × 重量单价, 最低一票)
用上面那个 900×2100 / 304 / 1.0 双包的例子:
ini
用料面积 = 3.40 × 1.08(直边损耗)= 3.67 ㎡
重量 = 3.67 × 7.93(不锈钢密度)× 1.0 × 1.10(包装加强系数)= 32.0 kg
打包体积:
总摊平宽 3 × 0.3195 = 0.96 m
捆数 = ceil(0.96 / 0.35) = 3 捆
体积 = 3 × 0.35 × 0.14 × 2.175(最长件)= 0.320 m³
按 180 元/m³ 和 1 元/kg 算:
- 按体积:0.320 × 180 = 57.6 元
- 按重量:32.0 × 1.0 = 32.0 元
- 最低一票:45 元
取大 → 57.6 元。
(成本汇总表里这一项最终是 59 元,差异来自捆数向上取整后的实际体积,量级一致。写这段是为了说明顺序,不是为了对齐到分。)
如果只按重量算,这单运费少算 25 块。单套 25 块听起来不多,但批量 20 套就是 500 块,而且是净亏------运费是实付的。
这里还有个隐藏点:必须先算体积,才能算运费。而体积依赖"最长件长度"和"总摊平宽度",这两个又来自件清单。所以物流费不能提到前面算,也不能拍一个比例。
口径六:七项成本要分项,不能拍一个数
完整的成本链长这样:
python
# ① 材料
用料面积 = 净展开面积 × 套数 × (1 + 损耗率)
板材费 = 用料面积 × 厚度单价[板厚] × 材质系数[材质]
# ② 折弯(直边和滚弯是两种工时,价格差几倍)
折弯费 = (折边米数 × 折边单价 + 滚弯米数 × 滚弯单价) × 套数
# ③ 焊接(分三种缝,不能一个系数了事)
焊接费 = 拼角数 × 角焊单价
+ 拼板处数 × 拼板单价
+ 拼角数 × 打磨单价 × (摊平宽度/1000)
+ (可选无痕满焊) 展开总长 × 满焊单价
# 满焊无缝:焊缝 = 全部展开总长
# 拼角焊: 焊缝 = 角焊缝 + 接料缝(处数 × 摊平宽)
# ④ 表面处理(弧形/异形上浮 15%------滚弯后镜面要返工)
表面费 = 净展开面积 × 表面单价 × 计价面数 × 弧形难度系数
# ⑤ 包装
包装费 = (单套木架 + 净展开面积/套数 × 每平米护边费) × 套数
# ⑥ 物流(见上)
# ⑦ 安装
安装费 = 单套安装费 × 套数 + (异地差旅)
拿样例 1(900×2100,D240,双包,304 / 1.0 / 拉丝 / 直边)实跑出来的结构:
| 项 | 金额 | 占比 |
|---|---|---|
| 板材 | 1601 | 72.3% |
| 折弯 | 128 | 5.8% |
| 焊接 | 60 | 2.7% |
| 表面 | 95 | 4.3% |
| 包装 | 121 | 5.5% |
| 物流 | 59 | 2.7% |
| 安装 | 150 | 6.8% |
| 总成本 | 2214 | |
| 建议报价(加成 30%) | 2879 | 实利 23.1% |
这张表最重要的不是金额,是那个 72.3%。
口径七:价表不能写死在引擎里,而且校准有优先级
引擎里只放公式,所有单价从 pricebook 查------引擎代码里一个价格数字都没有。
这不是洁癖。原因很实在:每个厂的底价都不一样,而且同一家厂的板材价一年要变好几次。价格写死在代码里,改一次价就要改代码、重新打包、重新发给客户。
但真正难的不是"把价表抽出来",而是告诉老板先校准哪几项。
回到那个 72.3%:
板材占七成,所以校准优先级是:厚度单价 > 表面单价 > 其余全部。
一个厂校准价表,如果按界面顺序从上往下填,他会在折弯、焊接上花两小时,而这两项加一起才占 8.5%。等填到板材已经没耐心了,随手填个大概------整个系统的准确度就毁在这一步。
所以界面上我做了"置信度"提示:进来先看板材和表面这两项填了没有,没填就标黄、算出来的价打低置信度标记。让老板知道"这个数现在还不能拿去报价"。
双引擎对数:防口径漂移
前端要实时(改一个参数价格立刻跳),后端要权威(出 PDF、存档、算账)。
所以我们跑了两套引擎:JS 的 engineLiteCalculate() 负责实时预览,Python 的 cost_engine.py 负责最终结果。两边公式同源,容差 1e-6,有 7 个用例 × 5 个指标的自检脚本。
这里踩过一个很典型的坑:
对数是拿两边算同一个输入比较结果。但如果 JS 用的是内置演示价表 、Python 用的是用户校准后的真实价表,两边结果必然不同------然后自检脚本会报"公式不同步"。
这不是公式不同步,是取数源不同步。
修法是:对数时从本地服务 /api/pricebook 取实时价表,显式传给 JS 引擎;取不到就报错退出,绝不静默退回演示价表。静默兜底是这类工具最坏的设计------它会把"我没取到数据"伪装成"两边算得不一样",让人去查一个不存在的问题。
还有一层:工艺告警
算得准只是及格。这个行业真正的痛点是算得准的活做不出来。
所以引擎跑完会顺手过一遍工艺规则:
| 条件 | 提示 |
|---|---|
| 弯曲半径 < max(25, 20 × 板厚) | 低于冷弯极限,会开裂 |
| 0.6 板 + 摊平宽 > 150mm | 平整度风险(波浪纹),建议 ≥ 0.8 |
| 201 + 镀钛 | 附着力/耐蚀差,建议改 304 |
| 摊平宽 > 最大板宽 | 需拼板 N 处,费用与损耗已计入 |
| 单包 + 墙厚远大于抱边深度 | 另一面要配收边条或改双包 |
| 镜面/镀钛 + 弧形 | 滚弯后需返工抛光,表面费已上浮 |
这些规则不参与算钱,但能拦住"报完价做完发现是废件"。对一个要给客户盖章的报价单来说,提前报警比算得准更值钱。
收尾:这套东西值多少钱,不取决于公式
七条口径写完,看上去很像那么回事。但我要说清楚一句:
公式决定这个系统"能不能用",价表决定它"值不值钱"。
同一份代码,配上准确的本地价表,能帮老板省下每次报价半小时、避免算错一单亏三天;配上随手填的价表,它输出的就是一份看起来很专业、实际不能用的 PDF。
所以这个系统真正的交付物不是代码,是"让老板在 20 分钟内把自己厂的价表填对"这件事。这也是我目前还没做好的部分。
你可以抄走的三件事
如果你也在给传统行业写报价/核价类工具,这三条跟行业无关,是通用的:
- 先找"投影面积 vs 实际用料"这类认知差。 行业里越是"大家都知道但没人写下来"的口径,越是你的护城河。它不在需求文档里,在你跟老板聊天时他随口说的那句"这个不能这么算"。
- 把口径歧义显式化,别替用户做决定。 加成率还是利润率、双包还是单包、同城还是异地------每个歧义都做成二选一,而不是猜一个默认值。猜错的代价是用户不再信你的数。
- 绝不静默兜底。 取不到实时价表就报错,别退回默认值。一个敢报错的工具,比一个永远有结果的工具更值得托付。
这套系统叫铁尺小匠,跑在 Windows 单机上,不联网。写它的起因很简单:厂里报价靠翻本子和问师傅,而这两件事都不该是报价的瓶颈。