FDE·案例复盘
基础目录
德莉玛 monorepo 订购单流水线 会员制 CRM 订购单系统(单体) 玻璃图纸与拆单 定价引擎 P0~P3

复盘 31 · 定价引擎 P0~P3 的校验与卡点复盘

这一篇只讲一件事:客户要「自动报价」,我没有直接开写引擎,而是拿 33,176 张真实历史订单 反查了一遍引擎的能力边界。查完的结果很硬——两个维度能打,两个维度是空的。真正的卡点不在算法,在三个前置数据。

项目锚点

站点:dlmware.cn,德莉玛门窗 / 淋浴房生产管理系统,业务流是订单 → BOM → 玻璃图纸 → 报价。
本篇对应的对象:P0~P3 四层切片里的 P1 定价引擎,以及把它卡住的三件 P0 地基。

33,176张真实历史订单
387家经销商
2,799个型号

相关基础篇:复盘 24 · 德莉玛 monorepo、复盘 25 · 订购单流水线、20 · 跨模块踩坑总账;29 · 订购单系统(单体)与 30 · 玻璃图纸导出 同批产出,讲的是这套系统的另外两面。

01

校验是怎么跑的:先别写代码,先拿真实数据打一枪

客户提出的需求是「自动报价」。一个工程师的本能反应是:把型号表读进来、把厚度映射做出来、把面积算上去、出价。但在动手之前,我先把客户库里的历史订单拉出来跑了一遍覆盖率校验。

校验的三个输入

输入规模在这台引擎里扮演什么
历史订单33,176 单校验样本本体。不是用来训练的,是用来量「引擎能认出多少」的
经销商387 家客户归属维度,决定报价走哪一套价格档
型号2,799 个型号识别的地基,号对不上,后面全是空算

校验方法(三条,都是笨办法但有效)

1

用 API 反向验证,不猜

拿历史单号逐个回查接口,看返回里字段到底是 null、是空串、还是真的有值。猜出来的覆盖率一定是错的,跑出来的才是数。

2

抽样统计,不抽样不认

从 33,176 单里抽 400 单做人工复核,把「有尺寸 / 没尺寸」逐单点出来。这一步的目的是发现「字段存在 ≠ 有数据」。

3

逐维度算命中率,不合并成一个漂亮的总数

型号、厚度、尺寸、客户归属四个维度分开算。合成一个数(比如「综合可用率 55%」)等于把坏消息洗掉了。

为什么第一件事是校验而不是开发:报价这个功能错了不会报错。它会安静地吐出一张看起来完全正常的报价单——数字是整数、格式是标准格式、打印出来谁都挑不出毛病。这种错比系统崩溃危险得多,因为崩溃会立刻被发现,错报价会一路走到客户现场。
02

四个匹配率:96.6 / 95.2 / 29 / 0,怎么读才不算自欺

这是整篇最核心的一张表。四个数字,两个能打、两个空心,中间没有模糊地带。

型号识别96.6%
玻璃厚度95.2%
尺寸维度29%
客户归属0%
维度真实命中率缺口在哪读出什么结论
型号识别96.6%2,799 个型号里能对上号的(出价的地基)可用:地基是实的,这块不用重做
玻璃厚度95.2%厚度档位基本齐全,余下 4.8% 是边缘值可用:边缘值走「标注 + 人工确认」即可
尺寸维度29%历史订单压根没填规格瓶颈:算面积、算玻璃、算型材全靠它
客户归属0%缺订单 ↔ 经销商 ID 对照表完全缺失:「这单属于哪个经销商档案」答不出(但「哪个客户买了多少单」答得出,见 28 篇第 5 张图)
客户归属这一格,我判错过一次(2026-10-03 更正):本篇原话是「连『这单是谁的』都答不出」。这句话是错的。
错在把「客户归属」理解成「客户 ↔ 经销商档案的关联」,于是卡在 customer_id 空值 100% 上得出「完全缺失」。但订单侧的 customer_name 本身就是可用的客户标识:302 个去重客户名(292 个形如「地区-姓名」,无空值、无过短异常),270 个客户买过 2 次以上 = 89.4% 的客户,贡献 99.9% 的订单,TOP10 客户占 24.6%。
所以真正缺的不是「这单是谁的」,是「这个客户对应经销商档案里的哪一家」——这里的说法我写错过一次(2026-10-03 更正):我原来写「对不上 customer.name,因为档案用 dlm001 编号、订单用「地区-姓名」自由文本,是两套编码」——方向对,但结论错:我假设了 name 列就是客户名,实际那列是编号。改成「把所有候选列逐个试、报出匹配率最高的那一列」之后,用客户表的 contact 列(也是「地区-姓名」格式)比对,302 个客户名里 264 个能对上 = 87.4%,两套编码基本能对上,不必做名称归一。
教训固化进产品:比对客户表时不能假设列名 ——「换个库就失效」的根本原因是「我以为我知道那一列是什么」。这条已经写进 products/retail-ai-audit/scripts/audit.py 的 linkage_check(),作为体检的固定检查项。
对本页定价结论的影响:无。定价引擎要的是「这个客户属于哪个价格档」,那确实需要外键,0% 就是 0%,这一点不变。但「用客户名做复购分析、做客户分层探索」这条路今天是通的,页面上不能写成「一单都答不了」。
这四个数字最容易被误读的地方:96.6% 和 95.2% 不等于「这个引擎已经能用了」。
它们只说明「认得出来」这一环成立;而「算得出价」还差三样东西——认得出来之后,价格从哪来、尺寸从哪读、这单算谁的。四个维度里有两个是空心的,那台引擎就只是台漂亮的计算器。
口径说明:命中率的分母是2,799 个型号(型号/厚度),不是 33,176 单。撒一个更大的分母去稀释低命中率,是技术汇报里最常见的自欺手法,我不做。分母写清楚,读者自己能判断。
03

转折点:400 单抽样里,97% 的 spec 是 null 或「否」

口径更正(2026-10-03):本篇说「spec = 尺寸」,这是错的——把全库 spec 取值分布打出来看,装的是规格与颜色(否 / 无 / 黑色 / 黑金沙 / 预埋),没有一条是尺寸。真正装尺寸的是 room 列,但它是混字段(形状 + 尺寸 + 单值 + 163 条「以图片为准」),必须用正则认:含「数字×数字」的 8,956 单 = 27.0%。详见 28 篇第 09 节的口径更正。
本篇「97% 的 spec 是 null 或「否」」这个观察本身仍然成立(那是 400 单抽样口径),错的是把 spec 的语义当成尺寸。

前两个数字让人乐观,抽样的那个数字把乐观直接打回原形。

抽样项真实结果这条说明了什么
抽样规模400 单不是全量,但足以推翻「字段应该都有值」的假设
spec 缺失97% 的 spec 是 null 或「否」(即无尺寸)字段在 schema 里写着,但生产数据里基本是空的
有尺寸的单只有 3 单1% 都不到,样本本身就不具代表性
这 3 单属于哪个系列K / Y 系列,不是 H / S / M 主流系列唯一有尺寸数据的样本,落在非主流系列上
结论(本篇的转折点):尺寸和订单 ↔ 客户对照表是空心的。
用这 400 单去跑定价引擎,跑不出任何一张靠谱的报价单——不是算法不够好,是输入本身就缺。这也解释了为什么上面尺寸维度只有 29%、客户归属是 0%:这两个数字不是「识别率低」,是「数据没有」。
这是技术人最容易误判的一类问题:接口是通的、字段类型是长的、数据库里字段也建了、文档里写明「支持按尺寸报价」。纸面上一切就绪,跑真实数据才发现压根没填。
97% 的缺失率,是从文档上看不出来的,只有跑数据才看得见。所以我的规矩是:任何「基于已有数据」的功能,开工前先跑一次覆盖率校验,校验报告比需求文档更能决定方案形状。
04

三个前置依赖:缺一个,引擎就只是台计算器

把上面的缺口收拢,只剩三件事。这三件事做不完,P1 引擎写出来也只能演示、不能出真价。

#前置依赖缺了会怎样(具体后果)数据从哪来优先级
①出厂价红线
(PV / KW / K1 这套价表)
认得出型号,但没有价可乘:面积算得再准,也乘不出钱。
部分系列只有 H / S / M 有价,K 系列要靠 BOM 模板兜底,H4 没有独立价,只能引用 H 或按模板折算
客户内部价表,必须书面给一版现行有效版本,不接受「你去系统里查」P0 · 🔴 阻塞
②尺寸数据源面积、玻璃、型材、工费全都算不了;29% 的覆盖意味着八成订单出不了面积要么历史补录规格,要么从图纸 / 下单明细反查,二选一,不能两个都要P0 · 🔴 阻塞
③订单 ↔ 经销商 ID 对照表客户归属 0%:不知道这单属于 387 家里的哪一家,就不知道该走哪个价格档、给谁返点、算谁的毛利客户方出对照表;缺失时按订单号反查经销商系统补,但一定要落一张正式表P0 · 🔴 阻塞
这三件事的共同点:没有一件是写代码的事。
① 是价表,② 是补数据,③ 是补一张关系表。但缺了任何一件,P1 引擎的全部产出都不可用。
所以我把它们全部划进 P0,而不是「先做引擎,顺带要数据」。后者是排期上最常见的偷懒:引擎先交付一个「看起来能用」的版本,等客户发现出不了真价,工期已经花掉了。

对照表:三种排法,我选第三条

排法做什么结果取舍
A 先引擎先把型号/厚度匹配做完,价表和数据边用边要三个月后交出一个能跑但出不了真价的引擎否:把数据问题伪装成交付问题
B 全做完再引擎等三个前置依赖全部齐了再动引擎交付时间完全押在客户给数据的时间上否:空窗期太长,谁都没底
C 分层(我选的)P0 数据地基 → P1 引擎 → P2 拆单与 BOM → P3 派单;P1 先用已验证的匹配能力出价,缺维度就显式标「算不出」依赖顺序清晰,P1 的匹配成果不会作废;P0 补齐即可直接出真价是:代价是 P0 阶段没有「报价」可看
C 的代价我也认:P0 阶段交付物看起来很素——一张对照表、一张价表、一份尺寸补录清单,没有一行让用户眼前一亮的界面。
但用三个月换一张对照表,和用三个月换一个跑不出真价的引擎,是两种完全不同的风险:前者风险已知且可控,后者风险在最后才能暴露,那时候已经在改合同了。
05

五个系列的价格覆盖:H / H4 / K / M / S,谁有价谁没价

出厂价红线(PV / KW / K1)不是一张表,是分档的。分档一拆开,缺口就露出来了。

系列价格覆盖没价时怎么办出价可信度
H已配价格—可直接出价
H4未配独立价引用 H 或按模板折算中低,必须标注「参考价」
K靠 BOM 模板兜底走 BOM 逐件折算,要先知道 BOM 里每个子件的件价才算得动中:依赖 BOM 模板本身准确
M已配价格—可直接出价
S已配价格—可直接出价
H4 这个系列值得单独说:它没有独立价,只能引用 H 或按模板折算。这意味着它的报价天然带折算假设——同一个 H4,用不同折算口径会得出不同的价。
我的处理:H4 一律带「参考价」标记出价,并且在报价单上写清折算口径。一个敢把假设写在脸上的报价单,比一个藏起假设的报价单有用得多,因为后者客户信,然后照着下单,然后亏钱。
06

两套栈:原型是 tRPC v11 + Node,生产是单体 server.js

这一层必须讲清楚,否则容易被误读成「两个系统不一致、是我没做好」。不是,是刻意物理分离的两套东西,跑在不同的层面上。

层次技术栈(真实)干什么用能不能跑真价
P0~P3 原型
(CPQ / BOM / 供应商引擎)
tRPC v11 + Node验证引擎逻辑:型号怎么认、厚度怎么映射、BOM 怎么拆、供应商怎么选不能:不接生产数据库、不带真价表
生产侧单体 server.js(8787)+ better-sqlite3 + pm2 + rsync真订单、真库存、真图纸、真价能,但前提是 P0 三件齐了
为什么必须分离:引擎逻辑要反复改,生产数据不能反复动。
在一个进程里同时改算法和动真实订单表,是最贵的一种调试方式——你改一行匹配逻辑,就得面对 33,176 单的历史数据,跑一次几十秒,改十次就是几小时,而且过程中还得保证生产库不被污染。
分两套之后,原型这一侧可以随便跑脏数据、随便造边界用例,生产库一个字节都不用动。
分离也带来一个必须盯住的债:两边的逻辑要保持一致,否则原型验证的结论不能迁移到生产。
我的规矩是匹配规则写在同一个函数里,只是数据源不同——原型喂假数据,生产喂真数据,规则代码是同一份。只要规则代码分叉了,前面所有校验数字就都不作数了。这条写进了跨模块踩坑总账。
生产侧那套栈本身的选择理由:单体 server.js、单文件库、pm2 守护 + rsync 推文件。这套组合简单到没有理由换——备份就是拷文件,迁移服务器就是拷文件,单机单进程跑得稳。不为「以后可能不够」提前引入分布式,是我在这个项目里守住的一条线。详见 复盘 24 · 德莉玛 monorepo;生产侧的工程细节(29 · 订购单系统(单体))与图纸侧(30 · 玻璃图纸导出与归档)是同一批的另外两篇。
07

数据没到位时怎么对齐认知:一张单文件 HTML

三个前置依赖没齐,引擎出不了真价,那还怎么推进?我的解法是:给干系人一张 shower-cpq-demo.html——无服务器、无价格数据、单文件。

约束为什么不能用真实系统演示
干系人打不开内网真实系统在内网地址上,对方根本进不去;一旦打不开,讨论直接卡死
系统里有真价对外演示不能露出真实出厂价红线,哪怕只是截图
真实系统一进去就是列表 + 筛选 + 分页不开服务器的人三分钟后还在找按钮,他看不到「引擎在干什么」
数据本身还没到位拿残缺数据演示,只会演示出一个错误结论

所以这个演示页长什么样

特性做法为什么这么做
单文件、无服务器一个 shower-cpq-demo.html,双击打开「打开就能看懂」是给不写代码的人看的东西的第一原则,不是「功能全」
无价格数据按 P0 / P1 分档演示,不填任何金额把「引擎逻辑」和「价格数据」这两件事在演示里切开,谁缺什么一眼可见
分档:P0 / P1P0 三件依赖做成待办态,P1 匹配逻辑做成可交互干系人能直接看见:要出真价,卡在这三件上
它是一份共识工具,不是产品用途是对齐认知,不是替代正式系统演示页用完就该退休,不能变成长期维护的第二套真相
这个手法本身比那张 HTML 更值钱:数据没到位时,不要拿假数据糊一个能跑的 demo 去「证明能力」,那样只会把三个数据卡点推迟到上线那天一起爆。
正确做法是:把「数据缺什么」做成演示页上的显眼位置,让每个打开它的人都看见同一个缺口。
这也是 P1 引擎必须做三态出价的原因:算得出(绿,显示金额区间并标注所用价)、算不出要补数据(黄,逐条列缺哪几项)、数据本身错(红,指出矛盾点)。只要「算不了」不是一个正式状态,一线就只剩三条路:自己估一个(最坏)、不用系统(等于白做)、回来找你(成本最高)。
08

「先看不要改任何东西」:只读诊断,是我给自己设的红线

校验期间客户说过一句原话:「先看,不要改任何东西。」这句话后来成了我在这条产线上的工作方式。

环节做了什么没做什么为什么
拉数据读订单、跑覆盖率、抽 400 单复核不写回任何字段补规格是客户业务动作,我替他补等于给脏数据背书
翻价表逐系列核价(PV / KW / K1)不新建、不覆盖任何价档价表是合同级数据,动一个档位要重新对账
跑原型在原型侧用假数据跑引擎逻辑不连生产库写数据见第 06 节的两套栈分离
出结论写成校验报告 + 三件 P0 依赖不含「我顺手改了点什么」诊断结论如果不附上「我没改任何东西」,就不可信
为什么把「不写」写进流程:只读诊断的价值,一半在结论,一半在「结论是可复现的」。
如果你在排查过程中顺手改过字段,那 33,176 单跑出来的覆盖率就不是数了——它混进了你的修改。下一次谁再跑一遍,会得到不一样的结果,报告就失去了权威。
所以我的规矩:诊断期输出只读报告;补数据、建对照表这些写操作,全部走客户确认后的独立批次,并且单独留一条对账记录。
顺带一句和排期有关的:数据地基最容易被低估,因为它的「工作量」大部分是等客户给数据、反复对字段,而这些时间不算在你写代码的时间里。
我现在的做法是把「等客户的时间」单独列一行——不算进开发工期,但写进交付承诺的日历。没有缓冲的排期,本质上是一份一定会失约的承诺。这条也写在 交付模板库 的诊断单模板里。
09

踩坑清单:这五个坑,全都是「跑数据之前看不见」的

#坑症状对策(已落地)
1字段存在 ≠ 有数据400 单里 97% 的 spec 是 null 或「否」,schema 和文档都写着「支持按尺寸报价」开工前必跑覆盖率校验,校验报告先于需求文档
2样本落在非主流系列上唯一有尺寸的 3 单是 K / Y 系列,不是 H / S / M 主流系列,导致 29% 这个数连趋势都不能代表抽样必须按系列分层抽,不能整池随机
3把坏消息合并成一个总数四个维度一平均,「综合可用率」看着还行四个维度分开报,分母写清楚(2,799 个型号)
4演示页变成第二套真相单文件演示被当成正经产品长期维护,规则与生产分叉演示页标注用途 = 对齐认知,规则代码与生产同源
5把数据问题当成工期问题引擎先交付一个「能跑但出不了真价」的版本,风险到最后才暴露三件依赖全划 P0,缺一件就不进 P1 出价
这五个坑有一个共同形状:它们在代码层面都是「对」的——接口返回 200、字段类型正确、逻辑无异常、测试全绿。错的全在输入侧。
这也是本篇最想留下的一句话:引擎出不出真价,是数据问题,不是代码问题。
一个 100% 正确的定价引擎,喂进去 null,吐出来的就是一张自信的错误报价单。
✓

自查清单

  • ✓我能报出校验的三组真实规模:33,176 单 / 387 家经销商 / 2,799 个型号
  • ✓我背得出四个匹配率:型号识别 96.6%、玻璃厚度 95.2%、尺寸维度 29%、客户归属 0%,并能说清分母是 2,799 个型号
  • ✓我讲得出抽样结论:400 单里 97% 的 spec 是 null 或「否」,只有 3 单有尺寸且属于 K / Y 系列
  • ✓我知道三个前置依赖是出厂价红线(PV / KW / K1)、尺寸数据源、订单 ↔ 经销商 ID 对照表,缺一不可
  • ✓我讲得清五个系列的价格覆盖:只有 H / S / M 已配价格,K 系列靠 BOM 模板兜底,H4 无独立价只能引用 H 或折算并标注「参考价」
  • ✓我说得出两套栈的边界:P0~P3 原型 = tRPC v11 + Node(不接生产库、不带真价);生产侧 = 单体 server.js(8787)+ better-sqlite3 + pm2 + rsync,物理分离刻意而为
  • ✓我讲得清 shower-cpq-demo.html 为什么是单文件 + 无服务器 + 无价格数据 + 按 P0/P1 分档
  • ✓我明白关键判断是「引擎出不出真价是数据问题,不是代码问题」,以及尺寸 29% / 客户 0% 的错不会报错,只会安静地输出错误报价单
  • ✓我知道诊断期执行只读(用户原话「先看不要改任何东西」),写操作全部走客户确认后的独立批次
  • ✓我认得这五个坑的共同形状:接口 200 / 类型正确 / 测试全绿,错的全在输入侧
技术基础 ← 回到基础目录,接着看 monorepo / 数据库原理 →
面试怎么答

题目:你做过报价 / 定价系统吗?最大的难点是什么?

做过,而且我第一件事不是写代码,是拿真实数据跑校验。德莉玛这条线的需求是「订单 → 拆单 → 报价 → 派单」全自动。我用他们库里的 33,176 张真实订单、387 家经销商、2,799 个型号 反查了一遍引擎边界。

结果是四个数:型号识别 96.6%、玻璃厚度 95.2%、尺寸维度 29%、客户归属 0%。前两个能打,后两个是空的。我特意把四个维度分开报,不合成一个「综合可用率」——合成总数就是把坏消息洗掉了,分母也必须写清楚是 2,799 个型号而不是 33,176 单。
真正把结论翻过来的,是 400 单抽样:97% 的 spec 是 null 或「否」,只有 3 单有尺寸,而且是 K / Y 系列,不是 H / S / M 主流系列。也就是说 29% 这个数连趋势都不能代表。

所以核心结论是:引擎出不出真价,是数据问题,不是代码问题。接口是通的、字段类型是长的、schema 里也写了、文档也写「支持按尺寸报价」,纸面上一切就绪,跑真实数据才发现压根没填。而且这种错不报错——它会安静地吐出一张格式标准、数字是整数、打印出来谁都挑不出毛病的报价单,一路走到客户现场才爆。这比系统崩溃危险得多。

我的方案是切成四层:P0 数据地基(订单 ↔ 经销商 ID 对照表、出厂价红线 PV/KW/K1、尺寸数据源)→ P1 引擎(型号 / 厚度匹配)→ P2 拆单与 BOM → P3 派单。这里最关键的判断是「P0 不做完,P1 做出来也是废的」。
排法上我否掉了两个:A 先引擎边用边要数据,三个月后交出一个能跑但出不了真价的东西,等于把数据问题伪装成交付问题;B 等数据齐了再做,交付时间完全押在客户给数据的时间上,空窗期太长谁都没底。我选分层:依赖顺序清晰,P1 的匹配成果不会作废,P0 一补齐就能直接出真价。
代价我也认:P0 阶段交付物很素——一张对照表、一张价表、一份补录清单,没有一行让人眼前一亮的界面。但用三个月换一张对照表,和用三个月换一个跑不出真价的引擎,是两种完全不同的风险。

价格这块有个具体的细节值得说:五个系列 H / H4 / K / M / S,只有 H / S / M 已配价格,K 系列靠 BOM 模板兜底,H4 没有独立价,只能引用 H 或按模板折算。所以 H4 一律带「参考价」标记出价,并且把折算口径写在报价单上。一个敢把假设写在脸上的报价单,比一个藏起假设的报价单有用得多——后者客户会信,然后照着下单,然后亏钱。

技术架构上有两点必须讲清楚,否则容易被当成不一致。第一,两套栈是刻意物理分离的:P0~P3 原型是 tRPC v11 + Node,专门跑引擎逻辑,不接生产库、不带真价;生产侧是单体 server.js(8787)+ better-sqlite3 + pm2 + rsync。为什么分?因为引擎逻辑要反复改,生产数据不能反复动——一个进程里同时改算法和动真实订单表,是最贵的调试方式。但要盯住一个债:两边的规则代码必须同源,只换数据源,否则前面所有校验数字都不作数。
第二,数据没到位时我给干系人的是一张 shower-cpq-demo.html:单文件、无服务器、无价格数据、按 P0/P1 分档。理由很硬:对方打不开内网、系统里有真价不能露、真实系统一进去就是列表筛选分页他三分钟还在找按钮、拿残缺数据演示只会演示出一个错误结论。这个手法本身比那张 HTML 值钱:不要拿假数据糊一个能跑的 demo 去证明能力,那会把三个数据卡点推迟到上线那天一起爆。

最后一条工作方法。客户说过一句原话:「先看,不要改任何东西。」我把这条立成了诊断期的红线:拉数据只读不写回、翻价表不新建不覆盖、原型不连生产库。原因是一半的价值在结论,一半在「结论可复现」——你在排查里顺手改过字段,那 33,176 单跑出来的就不是数了,下次谁跑一遍结果不一样,报告就失去权威。补数据、建对照表这些写操作,全部走客户确认后的独立批次,单独留对账记录。
顺带一条排期经验:数据地基的工作量大部分是等客户给数据、反复对字段,这些时间不算在写代码的时间里。我现在把「等客户的时间」单独列一行,不算开发工期,但写进交付承诺的日历。没有缓冲的排期,本质是一份一定会失约的承诺。

如果要挑一句当收尾,我挑这个:把「算不了」做成一个正式状态。报价必须有三态——算得出(绿,显示金额区间并标注所用价)、算不出要补数据(黄,逐条列出缺哪几项并能一键跳到采集表单)、数据本身错(红,指出矛盾点)。只要「算不了」不是正式状态,一线就只剩三条路:自己估一个、不用系统、回来找你,三条都是最坏结果。把它做成一等公民,等于让系统自己说「我不知道」,而不是让人替它瞎说。