Admin:规格匹配建议(规则层排序,人工确认) #90

Closed
opened 2026-08-10 10:37:30 +08:00 by ila · 9 comments
Owner

基本信息

  • 类型:需求(匹配辅助)
  • 父级大工单:#14 / 所属 MVP:#15
  • 前置:#88(映射改键必须先落地,本工单产出要写进 spec_mappings)
  • 设计:架构角色。[必须] 实施中发现实质性问题先停下来更新工单并重新审核,
    不要照着已知缺陷落地(CLAUDE.md §5/§7.5)

本工单已按 Codex 全栈评审(#90 评论)逐条修订。统计页面已按用户决定砍掉,
只保留数据记录,因此评审第 7 条(统计性能)随之作废。

要解决什么

#88 之后主链路通了,但人工匹配这一步实际不可用。

实测已采集的 PDD 商品,规格数分别是 48 / 120 / 30 个(平均 66)。
让操作员从 120 个候选里肉眼找出对应「灰色-小個子,L建議53-57公斤」的那一个,
每条订单都这么来,不现实。

两边文字对不齐(真实样本):

顺运宝   灰色-小個子,L建議53-57公斤
        蓝色,XL建議55-60公斤
        藍色,5XL建議100-120公斤        ← 同一批数据里繁简混用
        黑色常規款,L 建議52-60公斤

PDD     {color:'黑色中长款',        size:'L(80-115斤)'}
        {color:'【短袖裙】C1123粉',  size:'女M码(75-95斤)'}

四个差异:繁简混用、尺码单位不同(公斤 vs 斤,1:2)、
颜色带修饰词、PDD 尺码带前后缀。

为什么不上大模型

抽 200 条真实 product_spec 量了规则层能拿到的信号:

能提取出尺码码位(L / XL / 5XL / 均码…)   183 条  92%
带体重区间(公斤 或 斤)                    165 条  82%
有逗号可切分颜色/尺码                       198 条  99%

[注意] 这三个数字只说明「源文本里有信号」,不代表「推荐准确」。
真实准确率要靠本工单记录的采纳数据事后观测,不能拿这几个数当依据。

[必须] 本工单不引入任何大模型调用:

  1. 现在没人知道规则能覆盖多少——上来就引入 API key、网络依赖、按次成本
    和不确定性,是基于猜测设计(CLAUDE.md §3)
  2. 规则层的产出(候选打分排序)就是大模型层的输入格式,先做规则不浪费
  3. 本工单记录「预选是否被采纳」,跑一段时间就有真实命中率,
    那时再判断要不要补大模型,是基于数据而不是感觉

做什么 / 不做什么

做:

  1. 规则打分:把 PDD 候选按与顺运宝规格的匹配度排序
  2. 弹窗里按分数排序显示,符合门槛时预勾选,并显示推荐理由
  3. 只追加的决策审计表,记录建议与最终选择

不做(明确排除):

  • 不引入大模型 / 任何外部 AI 服务
  • 不自动保存映射——见红线
  • 不做统计页面。用户已确认砍掉:#15 明确把「统计报表」列为非范围,
    而「规则准不准」这个判断一辈子做一两次,一条 SELECT 就够,
    不值得为它做界面、去重、超时和基准测试。查询语句写进文档即可。
  • 不改 spec_mappings 表结构(#88 已定,本工单只新增审计表)
  • 不改采集链路、采购任务创建

红线:建议只排序,绝不裁决

[必须] 任何情况下都不得自动保存映射或自动创建采购任务。

匹配错 = 买错货 = 几周后真金白银才发现,是 CLAUDE.md §1 那张表里
代价最高的象限。本工单只做一件事:把最可能对的排在最前面,让人一眼确认。

[必须] 预勾选不等于已保存。必须点「保存」才写 spec_mappings。
即便只有一个候选、信号满分,也要人点。

[必须] 必须显示推荐理由,不能只给分数或默默排第一。理由让人能在
一秒内判断"这个推荐靠不靠谱",而不是盲信:

推荐  颜色分类:灰色中长款  尺码:L(106-114斤)        ★ 建议
      理由:尺码码位一致(L);体重区间 53-57公斤 = 106-114斤,
            被候选区间 80-115斤 完整覆盖;颜色主色一致(灰)

[必须] 界面不显示数字分数——显示数字会让人以为它有统计含义,
进而放松确认。只显示「★ 建议」标记和理由文字。

已确认的方案

① 结构化提取与模糊归一化必须分离

[必须] 固定流水线,顺序不能变:

1. 从**原文**提取结构化特征:码位、体重区间、主色
2. 另建一份用于修饰词比较的**文本副本**
3. 只对**副本**去括号、空白、装饰符
4. **原文始终用于展示**

[必须] 不要先去括号再提取。L(106-114斤) 去掉括号就把体重信息删了——
而那正是最强的信号。(初版工单写的"去掉装饰性括号内容用于辅助比较"
在这里会踩坑,以本条为准。)

[必须] 本工单的归一化函数不得替代或修改 #88 的 SpecKey() 和现有的
OptionKey()。前者是建议特征,后两者是数据身份���——混用会让
映射键随建议算法变化而漂移。

[必须] 繁简映射用显式表,不引入完整繁简转换库。数据里就那几十个字
(藍→蓝、個→个、規→规、長→长、碼→码、綠→绿、紅→红…),
一张表可读、可测、无依赖,且必须被测试逐条覆盖。

② 尺码:码位 + 体重区间双约束

码位提取:XS/S/M/L/XL/2XL…9XL/XXL/均码/F,两边都提。

[必须] 最长优先匹配并有边界测试——避免把 XXL 同时识别成 XL 和 L。
XXL 与 2XL 视为同一码位,XXXXXL 与 5XL 同理。
女M码、均码、F 都要覆盖。

体重区间:[必须] 统一换算成斤再比(1 公斤 = 2 斤)。
两边单位不同是实测事实,不换算直接比数字会得出「53 vs 80 差很远」的错误结论。

[必须] 区间比较不能只用 IoU。实测例子:

顺运宝 53-57公斤 = 106-114斤
PDD    L(80-115斤)
→ 106-114 被 80-115 完整覆盖,语义上是完美匹配
→ 但 IoU 只有约 23%,只看 IoU 会把它压到排不进前列

[必须] 同时计算四项,以「目标覆盖率」为主:

指标 定义 权重
目标覆盖率 交集 / 顺运宝区间 主
是否完整包含 顺运宝区间 ⊆ 候选区间 强加分
IoU 交集 / 并集 辅
中点距离 |中点差| 辅(用于同覆盖率时排序)

采购语义里「候选完整覆盖目标区间」通常是强信号。

[必须] 码位一致但体重区间完全不重叠 = 硬冲突(见④)。这多半是
PDD 那边码位命名不同(它的 L 实际对应顺运宝的 M),盲信码位会买错尺码。

③ 颜色:主色一致加分,主色冲突是硬冲突

[必须] 主色是「集合」,不是单值。 实测 3816 条不同规格原文里,
287 条的颜色部分含 2 个以上色词:

粉紅色                        → {粉, 红}
白灰拼黑【左上ZUM紅】常規短袖   → {灰, 白, 红, 黑}
米白色拼接卡其色               → {卡其, 白, 米}
5條裝(黑+黑+灰+灰+白)        → {灰, 白, 黑}

只取第一个色词会直接判错。

[必须] 最长色词优先。另有 116 条含「藏青」——不最长优先会被拆成「青」,
和「藏青」判成不同颜色。

[必须] 三分法,不是二分:

两边主色集合 判定
有交集 颜色不冲突,可作为一致信号(A 级需要这一条)
两边都非空且无交集 硬冲突——「黑色常規款」vs「白色中长款」字面只差一个字,但买错就是买错
任一边提不出颜色 未知:不算冲突,但不能形成 A 级强推荐

[建议] 剩余修饰词做子串/编辑距离,仅用于同层内排序。

④ 分层门槛,不编权重数字

[必须] 规则写成版本化、表驱动的确定性规则(如 rules_v1),
阈值和权重定义成有名字的常量并写清理由,不要散落魔数。

[必须] 不要编一组精确权重——没有带标签的数据支撑,编出来的数字是假精确。
但也不能留"最小分差"这种未定义的说法(初版正文一边说不编分数、一边要求
"第一名相对第二名达到最小差距",自相矛盾,无法实现也无法测试)。

第一版用确定的字典序层级,不需要任何分值:

层级 判定条件
A 级 码位一致 且 目标体重区间被完整覆盖 且 主色集合有交集
B 级 码位一致 且 体重区间部分重叠 且 主色不冲突
C 级 只有部分弱信号,或缺少码位/体重/颜色中的强信号
冲突级 主色集合明确无交集,或码位一致但体重完全不重叠

[必须] 只有下面四条全部满足才预勾选:

  1. 第一名是 A 级
  2. A 级候选只有一个(这就是"唯一领先"的确切定义)
  3. 没有无法判断的额外规格维度(见⑤)
  4. 该候选无硬冲突

[必须] 多个 A 级并列 → 只稳定置顶,不预勾选;B / C 级永不预勾选。

[必须] IoU、中点距离、修饰词相似度只用于同层内稳定排序,
不得把 B/C 提升为 A。

[必须] 第一版优先"少错"而不是"多覆盖"。颜色或尺码推荐错了最终会进入
采购任务,覆盖率低但准确更安全。

[必须] 排序确定且可测:同分时按 PDD 原始顺序稳定排序,
多次刷新顺序不变。

⑤ 多维规格必须安全处理

PDD 支持任意多个维度,不只有 color/size。

[必须] 若两个候选颜色尺码相同、只在第三维度不同(款式/长度/套餐…),
规则不能任意预选其中一个——取消预勾选,理由写「存在无法判断的额外规格维度」。

[必须] 候选必须复用现有的 pddOptionChoices,不要另写一套解析。
它已经过滤了 available=false、空组合和重复 OptionKey——
另写一套可能推荐不可购买或已下架的规格。

⑥ 决策审计表(只追加)

[必须] 不要把建议信息作为两列挂在 spec_mappings 上。那张表是 upsert ���,
后续保存会覆盖旧决策,无法知道:同一映射改过几次、当时用哪版规则、
新旧规则哪个更好。

新建只追加的审计表(MySQL v4):

CREATE TABLE IF NOT EXISTS spec_mapping_decisions (
    id                   BIGINT AUTO_INCREMENT PRIMARY KEY,
    shopee_goods_id      VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
    spec_key             VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
    pdd_goods_id         VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
    rules_version        VARCHAR(32)  NOT NULL,
    suggested_option_key VARCHAR(191) COLLATE utf8mb4_bin,  -- NULL = 当时无建议
    chosen_option_key    VARCHAR(191) COLLATE utf8mb4_bin NOT NULL,
    accepted             TINYINT NOT NULL,   -- 1=采纳建议 0=未采纳或当时无建议
    decided_by           VARCHAR(191),
    decided_at           VARCHAR(35) NOT NULL,
    KEY idx_decisions_mapping (shopee_goods_id, spec_key, pdd_goods_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

[必须] DDL 里加 CHECK (accepted IN (0, 1))。

[必须] 只写不读,不参与任何业务判断。用途只有一个���
事后观察「成功保存决策中的建议采纳情况」。

[必须] suggested_option_key 为 NULL 时 accepted 记 0
(当时没建议,谈不上采纳)。查采纳率时必须排除这类,见下面的 SQL。

[必须] rules_version / suggested_option_key / chosen_option_key
写入前显式校验列宽,禁止 MySQL 截断。实测当前 OptionKey 最长
52 字符,191 够用——但不能依赖当前样本,规格文字随时可能变长。

⑦ 保存路径:同事务 + 服务端重算,不信任表单

[必须] spec_mappings upsert 与 spec_mapping_decisions insert
必须共用 SaveSybMapping 的同一个事务。

  • 任一步失败 → 映射和审计一起回滚
  • 同一映射连续改两次 → 成功提交两次才产生 2 条审计
  • 不点保存 / 校验失败 / 事务回滚 → 都不得留下审计

[必须] 集成测试要有一条「故意让审计写入失败,断言映射也没有更新」。

[必须] 服务端不得相信表单提交的 suggested_option_key / accepted /
rules_version。
浏览器表单可改;而且弹窗打开后 PDD 可能被重新采集,
直接采信会让审计记下一个用户从没看见过的建议。

保存时在事务内:

1. 重新读取当前顺运宝明细、PDD 商品和可购买候选
2. 用服务端当前的 rules_version 重跑建议规则
3. 确认用户选的 option 仍存在且 available = true
4. 由服务端计算 suggested_option_key 和 accepted
5. 原子写映射 + 审计

[必须] 弹窗展示期间 PDD 规格已变化时,拒绝保存并提示刷新,
不要把"当前重算结果"冒充成用户当时看到的建议。

[建议] 陈旧检测不需要另造哈希机制——pdd_products.updated_at
就是现成的版本标记(每次采集结果写入都会变)。弹窗渲染时带上它,
保存时比对,不相等就拒绝。

[必须] 迁移在 mysql_db.go 追加 mysqlSchemaV4,
V1~V3 一个字节不许改;自检通过才记版本;
DDL 完成但版本未记录时重跑要幂等(CREATE TABLE IF NOT EXISTS);
MySQL 专用自检清单加入本表。理由同 #88 §⑥⑦。

[必须] 文档里写清怎么查采纳率(一条 SQL),代替被砍掉的统计页面:

-- 采纳率:`[必须]` 只统计真正给出过建议的决策。
-- 把"当时无建议"(accepted 记 0)算进分母,会把它误统计成"建议被拒绝",
-- 采纳率被系统性低估。
SELECT rules_version,
       COUNT(*)                                   AS 有建议的决策数,
       SUM(accepted)                              AS 采纳数,
       ROUND(SUM(accepted) / COUNT(*) * 100, 1)   AS 采纳率
FROM spec_mapping_decisions
WHERE suggested_option_key IS NOT NULL
GROUP BY rules_version;

-- 无建议的条数单独查,它衡量的是"规则覆盖率",不是"准确率"
SELECT rules_version, COUNT(*) AS 无建议决策数
FROM spec_mapping_decisions
WHERE suggested_option_key IS NULL
GROUP BY rules_version;

实施前补充(2026-08-10,Codex 第三轮复核)

本节补齐自动预勾选的安全边界、陈旧保存保护和审计口径;与前文冲突时以本节为准。

A. 陈旧检测使用完整匹配上下文,而非只看 PDD 更新时间

弹窗渲染时由服务端生成 context_version(稳定序列化后做 SHA-256),至少包含:

  • 顺运宝明细稳定 ID、当前 spec_key 和该明细 updated_at;
  • 当前关联的 pdd_goods_id;
  • pdd_products.updated_at;
  • 当前服务端 rules_version。

保存时服务端重新读取以上字段并重算版本;不一致即拒绝保存,提示“数据或匹配规则已变化,请刷新后重新核对”。表单中的版本只作为乐观并发令牌,不作为建议结论;suggested_option_key、accepted、rules_version 仍全部由服务端重算。这样同时覆盖顺运宝重同步、PDD 改绑/重采和规则发布。

B. A 级增加超宽区间安全门槛

“完整覆盖”不能让 40-200斤 这类过宽候选自动预勾目标 106-114斤。rules_v1 增加具名常量:

  • maxCandidateWidthRatio = 5;
  • minCandidateWidthAllowanceJin = 40;
  • maxMidpointDistanceJin = 20。

候选区间宽度必须 <= max(目标区间宽度 × 5, 40斤),且候选与目标中点距离 <= 20斤,才允许维持 A 级;否则降为 B/C 且不得预勾。该门槛保留工单既有 106-114斤 被 80-115斤 覆盖的 A 级样本,同时拦截明显宽泛区间。阈值属于 rules_v1,以后只能通过新规则版本调整。

C. 颜色采用“最长词命中后展开规范色集合”

最长词优先只负责分词,命中复合词后还要展开规范色集合,避免 粉红 与 粉色 被误判冲突:

  • 粉红/粉紅/浅粉/淺粉/���粉/粉色 → {粉};
  • 米白 → {米, 白};
  • 藏青/藏藍 → {藏青},不得拆成 青;
  • 其他繁体字先走正文的显式繁简表,再进入颜色词表。

每条别名和复合色展开都必须有表驱动测试。只有规范色集合双方非空且无交集才是颜色硬冲突。

D. 明确识别 PDD 维度

复用 pddOptionChoices 生成候选后,按维度名显式识别:

  • 颜色别名:color、colour、颜色、颜色分类、色系;
  • 尺码别名:size、尺码、尺寸、大小、码数。

比较前对维度名做 trim、大小写和繁简规范化。出现多个疑似颜色维度、多个疑似尺码维度,或存在无法判断且会区分候选的额外维度时,取消预勾并显示原因。

E. 审计语义与迁移自检

accepted=0 的注释改为“未采纳建议或当时无建议”,不得写成“人工改了”。文档中的指标只能称“成功保存决策中的建议采纳率”,不能称规则准确率;关闭弹窗和未保存选择不在样本中。

v4 自检除表存在外,还必须验证关键列长度/可空性、accepted CHECK 和索引形状;错误结构不得记录版本。页面回归沿用 #88 的角色口径:管理员六页正常,采购员权限隔离正确。

预计修改文件

文件 改什么
admin/service/specmatch.go + _test.go 新建:归一化、码位/体重/主色提取、分层门槛、理由生成
admin/service/purchase_workflow.go 候选排序与建议标记;SaveSybMapping 写审计
admin/repository/mysql_db.go mysqlSchemaV4(审计表);MySQL 专用自检清单
admin/repository/mysql_db_integration_test.go v3→v4 升级 + 断点重跑
admin/repository/mapping.go 写审计表
admin/model/model.go 审计结构
admin/handler/web/others.go + admin/templates/syb/* 弹窗排序/预勾选/理由/核对提示
docs/admin/01/03/05 匹配规则、表结构、界面;采纳率查询 SQL

验收标准

归一化与提取

  • 繁简映射表被测试逐条覆盖;藍色 与 蓝色 归一化后相等
  • 先提取后去括号:L(106-114斤) 的体重信息没有被去括号弄丢
  • 码位最长优先:XXL 不被识别成 XL/L;XXL≡2XL,XXXXXL≡5XL
  • 女M码 / 均码 / F 均能提取
  • 归一化函数不改动 SpecKey / OptionKey 的行为(有测试断言身份键不变)
  • 展示用原文,不是归一化后的文本

体重区间

  • 建議53-57公斤 换算成 106-114斤
  • 106-114 与 80-115 判为完整包含,覆盖率 100%,排第一(不因 IoU 低被压)
  • 码位一致但区间完全不重叠 → ���冲突,不预勾选,理由点出冲突

颜色

  • 灰色-小個子 与 灰色中长款 主色判为一致
  • 主色冲突(黑 vs 白)→ 硬冲突,排不进前列且不预勾选
  • 多色集合有交集 → 不冲突(粉紅色 vs 粉色系 视为一致)
  • 两边集合都非空且无交集才判硬冲突
  • 任一边提不出颜色 → 未知:不算冲突,但不能形成 A 级
  • 最长色词优先:藏青 不被拆成 青

分层门槛与红线

  • A/B/C/冲突四级判定不依赖任何未定义的分数或分差
  • 只有「唯一 A 级」才预勾选;多个 A 级并列 → 只置顶不预勾
  • B / C 级永不预勾选
  • IoU / 中点距离只用于同层排序,不能把 B/C 提成 A
  • 全部候选有硬冲突 → 不建议任何一项
  • 修正后的「完整包含」「部分重叠」「无码位无主色」三个样本按层级正确判定
  • 排序稳定(多次刷新顺序不变)
  • 界面不显示数字分数
  • 不点「保存」直接关闭弹窗 → spec_mappings 无新增(硬性测试)
  • 保存按钮旁有「请核对颜色、尺码和体重范围」提示
  • 不自动创建采购任务

多维规格

  • 两候选仅第三维度不同 → 取消预勾选,理由说明存在无法判断的额外维度
  • 候选来自 pddOptionChoices,不含 available=false 的规格

审计与迁移

  • 保存映射时写入审计表;同一映射改两次产生 2 行(只追加)
  • 人工选了非第一候选 → accepted=0
  • 当时无建议 → suggested_option_key 为 NULL 且 accepted=0
  • 映射 upsert 与审计 insert 同事务;故意让审计失败时映射也回滚(有测试)
  • 篡改表单里的 suggested / accepted / rules_version 不影响服务端审计结论(有测试)
  • 弹窗打开后 PDD 规格变化 → 拒绝陈旧保存并提示刷新
  • accepted 的 CHECK 生效;option key 超长被应用层明确拒绝,不截断
  • 采纳率 SQL 排除 suggested_option_key IS NULL
  • mysqlSchemaV4 追加,V1~V3 逐字未动
  • v3 → v4 升级测试通过;DDL 完成未记版本时重跑幂等
  • 自检失败时不记录版本

其他

  • 没有统计页面(本工单已砍);采纳率查询 SQL 写进文档
  • 管理员六个业务页面均正常;采购员可访问其授权页面且不能进入用户管理
  • GOTOOLCHAIN=go1.23.0 下 go vet / gofmt -l . / go test ./... 全过

怎么验证

cd D:\chengma\cmautobuy\admin
$env:GOTOOLCHAIN="go1.23.0"
go vet ./...; gofmt -l .; go test ./... -count=1
Remove-Item Env:GOTOOLCHAIN

[必须] 单元测试必须建脱敏、人工确认过的带标签样本集,表驱动。
每条样本要标注:应推荐哪个 / 是否允许预勾 / 理由须包含哪些事实。

[必须] 至少覆盖这些类别(不是只测能跑通的那几条):

类别              样本                                        期望
正确第一名        灰色-小個子,L建議53-57公斤 vs {灰色中长款,L(106-114斤)}   排第一,预勾
繁简+单位换算     藍色,5XL建議100-120公斤 vs {蓝色长款,5XL(200-240斤)}     排第一,预勾
体重完整包含      灰色,L建議53-57公斤 vs {灰色, L(80-115斤)}  唯一 A 级,预勾
体重部分重叠      灰色,L建議55-60公斤(=110-120斤)
                  vs {灰色, L(115-130斤)}                    B 级,只置顶不预勾
无码位无主色      53-57公斤 vs L(80-115斤)                   C 级,不预勾(缺强信号)
体重完全不重叠    L建議52-60公斤 vs L(200-240斤)             硬冲突,不预勾
颜色冲突          黑色常規款 vs 白色中长款                      硬冲突,排不进前列
多维歧义          两候选仅"款式"不同                           取消预勾,理由说明
无单位/无码位     信息不足的规格原文                            不预勾,提示人工核对

端到端:挑一条已采集且规格数 ≥ 48 的商品的待匹配明细,打开弹窗,
���认排序与预勾选符合门槛、理由可读;不点保存直接关闭,
确认 spec_mappings 与审计表都没有新增行。

交付报告必须贴出带标签样本集的实际测试输出,以及"不点保存不写库"的验证。

风险和回退

风险 应对
推荐错了被盲信 → 买错货 不自动保存;必须人工点;强制显示理由;硬冲突、非唯一 A 级或信号不足时不预勾选
只看 IoU,完整包含被压低 覆盖率优先,已列验收项并给了具体样本
去括号丢掉体重信息 先提取后去括号,已列验收项
主色冲突却因字面相似排前 主色冲突设为硬冲突
多维规格任意预选 第三维度不同时取消预勾选
推荐了不可购买的规格 强制复用 pddOptionChoices
归一化污染了身份键 明确分离,有测试断言 SpecKey/OptionKey 不变
又一次原地改迁移 只追加 + 断点重跑测试

回退:git revert,回到纯人工挑选,已保存的映射语义不变。
已迁到 v4 的库回退后会因「版本高于程序支持」拒绝启动——
[必须] 与 #88 同理,正式实施前准备好 MySQL 逻辑备份和恢复命令,
不要只依赖 git revert。

## 基本信息 - 类型:需求(匹配辅助) - 父级大工单:#14 / 所属 MVP:#15 - **前置:#88**(映射改键必须先落地,本工单产出要写进 `spec_mappings`) - 设计:架构角色。`[必须]` 实施中**发现实质性问题先停下来更新工单并重新审核**, 不要照着已知缺陷落地(CLAUDE.md §5/§7.5) > 本工单已按 Codex 全栈评审(#90 评论)逐条修订。**统计页面已按用户决定砍掉**, > 只保留数据记录,因此评审第 7 条(统计性能)随之作废。 ## 要解决什么 `#88` 之后主链路通了,但**人工匹配这一步实际不可用**。 实测已采集的 PDD 商品,规格数分别是 **48 / 120 / 30 个**(平均 66)。 让操作员从 120 个候选里肉眼找出对应「灰色-小個子,L建議53-57公斤」的那一个, 每条订单都这么来,不现实。 两边文字对不齐(真实样本): ```text 顺运宝 灰色-小個子,L建議53-57公斤 蓝色,XL建議55-60公斤 藍色,5XL建議100-120公斤 ← 同一批数据里繁简混用 黑色常規款,L 建議52-60公斤 PDD {color:'黑色中长款', size:'L(80-115斤)'} {color:'【短袖裙】C1123粉', size:'女M码(75-95斤)'} ``` 四个差异:**繁简混用**、**尺码单位不同(公斤 vs 斤,1:2)**、 **颜色带修饰词**、**PDD 尺码带前后缀**。 ## 为什么不上大模型 抽 200 条真实 `product_spec` 量了规则层能拿到的信号: ```text 能提取出尺码码位(L / XL / 5XL / 均码…) 183 条 92% 带体重区间(公斤 或 斤) 165 条 82% 有逗号可切分颜色/尺码 198 条 99% ``` `[注意]` **这三个数字只说明「源文本里有信号」,不代表「推荐准确」。** 真实准确率要靠本工单记录的采纳数据事后观测,不能拿这几个数当依据。 `[必须]` **本工单不引入任何大模型调用**: 1. 现在没人知道规则能覆盖多少——上来就引入 API key、网络依赖、按次成本 和不确定性,是基于猜测设计(CLAUDE.md §3) 2. 规则层的产出(候选打分排序)**就是大模型层的输入格式**,先做规则不浪费 3. 本工单记录「预选是否被采纳」,跑一段时间就有真实命中率, 那时再判断要不要补大模型,是基于数据而不是感觉 ## 做什么 / 不做什么 做: 1. 规则打分:把 PDD 候选按与顺运宝规格的匹配度排序 2. 弹窗里按分数排序显示,**符合门槛时**预勾选,并显示**推荐理由** 3. 只追加的**决策审计表**,记录建议与最终选择 不做(明确排除): - **不引入大模型 / 任何外部 AI 服务** - **不自动保存映射**——见红线 - **不做统计页面**。用户已确认砍掉:`#15` 明确把「统计报表」列为非范围, 而「规则准不准」这个判断一辈子做一两次,一条 `SELECT` 就够, 不值得为它做界面、去重、超时和基准测试。**查询语句写进文档即可。** - 不改 `spec_mappings` 表结构(`#88` 已定,本工单**只新增审计表**) - 不改采集链路、采购任务创建 ## 红线:建议只排序,绝不裁决 `[必须]` **任何情况下都不得自动保存映射或自动创建采购任务。** 匹配错 = 买错货 = **几周后真金白银才发现**,是 CLAUDE.md §1 那张表里 代价最高的象限。本工单只做一件事:把最可能对的排在最前面,让人一眼确认。 `[必须]` 预勾选**不等于已保存**。必须点「保存」才写 `spec_mappings`。 即便只有一个候选、信号满分,也要人点。 `[必须]` **必须显示推荐理由**,不能只给分数或默默排第一。理由让人能在 一秒内判断"这个推荐靠不靠谱",而不是盲信: ```text 推荐 颜色分类:灰色中长款 尺码:L(106-114斤) ★ 建议 理由:尺码码位一致(L);体重区间 53-57公斤 = 106-114斤, 被候选区间 80-115斤 完整覆盖;颜色主色一致(灰) ``` `[必须]` **界面不显示数字分数**——显示数字会让人以为它有统计含义, 进而放松确认。只显示「★ 建议」标记和理由文字。 ## 已确认的方案 ### ① 结构化提取与模糊归一化必须分离 `[必须]` 固定流水线,顺序不能变: ```text 1. 从**原文**提取结构化特征:码位、体重区间、主色 2. 另建一份用于修饰词比较的**文本副本** 3. 只对**副本**去括号、空白、装饰符 4. **原文始终用于展示** ``` `[必须]` **不要先去括号再提取**。`L(106-114斤)` 去掉括号就把体重信息删了—— 而那正是最强的信号。(初版工单写的"去掉装饰性括号内容用于辅助比较" 在这里会踩坑,以本条为准。) `[必须]` 本工单的归一化函数**不得替代或修改** `#88` 的 `SpecKey()` 和现有的 `OptionKey()`。前者是**建议特征**,后两者是**数据身份���**——混用会让 映射键随建议算法变化而漂移。 `[必须]` **繁简映射用显式表**,不引入完整繁简转换库。数据里就那几十个字 (`藍→蓝`、`個→个`、`規→规`、`長→长`、`碼→码`、`綠→绿`、`紅→红`…), 一张表可读、可测、无依赖,且**必须被测试逐条覆盖**。 ### ② 尺码:码位 + 体重区间双约束 **码位提取**:`XS/S/M/L/XL/2XL…9XL/XXL/均码/F`,两边都提。 `[必须]` **最长优先匹配并有边界测试**——避免把 `XXL` 同时识别成 `XL` 和 `L`。 `XXL` 与 `2XL` 视为同一码位,`XXXXXL` 与 `5XL` 同理。 `女M码`、`均码`、`F` 都要覆盖。 **体重区间**:`[必须]` **统一换算成斤再比**(1 公斤 = 2 斤)。 两边单位不同是实测事实,不换算直接比数字会得出「53 vs 80 差很远」的错误结论。 `[必须]` **区间比较不能只用 IoU**。实测例子: ```text 顺运宝 53-57公斤 = 106-114斤 PDD L(80-115斤) → 106-114 被 80-115 完整覆盖,语义上是完美匹配 → 但 IoU 只有约 23%,只看 IoU 会把它压到排不进前列 ``` `[必须]` 同时计算四项,**以「目标覆盖率」为主**: | 指标 | 定义 | 权重 | |---|---|---| | **目标覆盖率** | 交集 / 顺运宝区间 | **主** | | 是否完整包含 | 顺运宝区间 ⊆ 候选区间 | 强加分 | | IoU | 交集 / 并集 | 辅 | | 中点距离 | \|中点差\| | 辅(用于同覆盖率时排序) | 采购语义里「候选完整覆盖目标区间」通常是强信号。 `[必须]` **码位一致但体重区间完全不重叠 = 硬冲突**(见④)。这多半是 PDD 那边码位命名不同(它的 L 实际对应顺运宝的 M),盲信码位会买错尺码。 ### ③ 颜色:主色一致加分,主色冲突是硬冲突 `[必须]` **主色是「集合」,不是单值。** 实测 3816 条不同规格原文里, **287 条**的颜色部分含 2 个以上色词: ```text 粉紅色 → {粉, 红} 白灰拼黑【左上ZUM紅】常規短袖 → {灰, 白, 红, 黑} 米白色拼接卡其色 → {卡其, 白, 米} 5條裝(黑+黑+灰+灰+白) → {灰, 白, 黑} ``` 只取第一个色词会直接判错。 `[必须]` **最长色词优先**。另有 **116 条**含「藏青」——不最长优先会被拆成「青」, 和「藏青」判成不同颜色。 `[必须]` 三分法,不是二分: | 两边主色集合 | 判定 | |---|---| | **有交集** | 颜色不冲突,可作为一致信号(A 级需要这一条) | | **两边都非空且无交集** | **硬冲突**——「黑色常規款」vs「白色中长款」字面只差一个字,但买错就是买错 | | **任一边提不出颜色** | **未知**:不算冲突,但**不能形成 A 级强推荐** | `[建议]` 剩余修饰词做子串/编辑距离,仅用于同层内排序。 ### ④ 分层门槛,不编权重数字 `[必须]` 规则写成**版本化、表驱动**的确定性规则(如 `rules_v1`), 阈值和权重定义成**有名字的常量并写清理由**,不要散落魔数。 `[必须]` **不要编一组精确权重**——没有带标签的数据支撑,编出来的数字是假精确。 但也**不能留"最小分差"这种未定义的说法**(初版正文一边说不编分数、一边要求 "第一名相对第二名达到最小差距",自相矛盾,无法实现也无法测试)。 第一版用**确定的字典序层级**,不需要任何分值: | 层级 | 判定条件 | |---|---| | **A 级** | 码位一致 **且** 目标体重区间被**完整覆盖** **且** 主色集合**有交集** | | **B 级** | 码位一致 **且** 体重区间**部分重叠** **且** 主色不冲突 | | **C 级** | 只有部分弱信号,或缺少码位/体重/颜色中的强信号 | | **冲突级** | 主色集合明确**无交集**,或码位一致但体重**完全不重叠** | `[必须]` **只有下面四条全部满足才预勾选**: 1. 第一名是 **A 级** 2. **A 级候选只有一个**(这就是"唯一领先"的确切定义) 3. 没有无法判断的额外规格维度(见⑤) 4. 该候选无硬冲突 `[必须]` **多个 A 级并列 → 只稳定置顶,不预勾选**;**B / C 级永不预勾选**。 `[必须]` IoU、中点距离、修饰词相似度**只用于同层内稳定排序**, **不得把 B/C 提升为 A**。 `[必须]` **第一版优先"少错"而不是"多覆盖"**。颜色或尺码推荐错了最终会进入 采购任务,覆盖率低但准确更安全。 `[必须]` 排序**确定且可测**:同分时按 PDD 原始顺序稳定排序, 多次刷新顺序不变。 ### ⑤ 多维规格必须安全处理 PDD 支持任意多个维度,不只有 color/size。 `[必须]` 若两个候选**颜色尺码相同、只在第三维度不同**(款式/长度/套餐…), 规则**不能任意预选其中一个**——取消预勾选,理由写「存在无法判断的额外规格维度」。 `[必须]` 候选**必须复用现有的 `pddOptionChoices`**,不要另写一套解析。 它已经过滤了 `available=false`、空组合和重复 `OptionKey`—— 另写一套可能推荐**不可购买或已下架**的规格。 ### ⑥ 决策审计表(只追加) `[必须]` **不要把建议信息作为两列挂在 `spec_mappings` 上**。那张表是 upsert ���, 后续保存会**覆盖旧决策**,无法知道:同一映射改过几次、当时用哪版规则、 新旧规则哪个更好。 新建只追加的审计表(MySQL v4): ```sql CREATE TABLE IF NOT EXISTS spec_mapping_decisions ( id BIGINT AUTO_INCREMENT PRIMARY KEY, shopee_goods_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL, spec_key VARCHAR(191) COLLATE utf8mb4_bin NOT NULL, pdd_goods_id VARCHAR(191) COLLATE utf8mb4_bin NOT NULL, rules_version VARCHAR(32) NOT NULL, suggested_option_key VARCHAR(191) COLLATE utf8mb4_bin, -- NULL = 当时无建议 chosen_option_key VARCHAR(191) COLLATE utf8mb4_bin NOT NULL, accepted TINYINT NOT NULL, -- 1=采纳建议 0=未采纳或当时无建议 decided_by VARCHAR(191), decided_at VARCHAR(35) NOT NULL, KEY idx_decisions_mapping (shopee_goods_id, spec_key, pdd_goods_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ``` `[必须]` DDL 里加 **`CHECK (accepted IN (0, 1))`**。 `[必须]` **只写不读,不参与任何业务判断**。用途只有一个��� 事后观察「成功保存决策中的建议采纳情况」。 `[必须]` `suggested_option_key` 为 NULL 时 `accepted` 记 **0** (当时没建议,谈不上采纳)。查采纳率时必须排除这类,见下面的 SQL。 `[必须]` `rules_version` / `suggested_option_key` / `chosen_option_key` **写入前显式校验列宽,禁止 MySQL 截断**。实测当前 `OptionKey` 最长 52 字符,191 够用——但不能依赖当前样本,规格文字随时可能变长。 ### ⑦ 保存路径:同事务 + 服务端重算,不信任表单 `[必须]` **`spec_mappings` upsert 与 `spec_mapping_decisions` insert 必须共用 `SaveSybMapping` 的同一个事务。** - 任一步失败 → 映射和审计**一起回滚** - 同一映射连续改两次 → 成功提交两次才产生 **2 条**审计 - 不点保存 / 校验失败 / 事务回滚 → **都不得留下审计** `[必须]` 集成测试要有一条「**故意让审计写入失败,断言映射也没有更新**」。 `[必须]` **服务端不得相信表单提交的 `suggested_option_key` / `accepted` / `rules_version`。** 浏览器表单可改;而且弹窗打开后 PDD 可能被重新采集, 直接采信会让审计记下一个**用户从没看见过的建议**。 保存时在事务内: ```text 1. 重新读取当前顺运宝明细、PDD 商品和可购买候选 2. 用服务端当前的 rules_version 重跑建议规则 3. 确认用户选的 option 仍存在且 available = true 4. 由服务端计算 suggested_option_key 和 accepted 5. 原子写映射 + 审计 ``` `[必须]` **弹窗展示期间 PDD 规格已变化时,拒绝保存并提示刷新**, 不要把"当前重算结果"冒充成用户当时看到的建议。 `[建议]` 陈旧检测**不需要另造哈希机制**——`pdd_products.updated_at` 就是现成的版本标记(每次采集结果写入都会变)。弹窗渲染时带上它, 保存时比对,不相等就拒绝。 `[必须]` 迁移在 `mysql_db.go` 追加 **`mysqlSchemaV4`**, `V1`~`V3` 一个字节不许改;**自检通过才记版本**; **DDL 完成但版本未记录时重跑要幂等**(`CREATE TABLE IF NOT EXISTS`); MySQL 专用自检清单加入本表。理由同 #88 §⑥⑦。 `[必须]` 文档里写清**怎么查采纳率**(一条 SQL),代替被砍掉的统计页面: ```sql -- 采纳率:`[必须]` 只统计真正给出过建议的决策。 -- 把"当时无建议"(accepted 记 0)算进分母,会把它误统计成"建议被拒绝", -- 采纳率被系统性低估。 SELECT rules_version, COUNT(*) AS 有建议的决策数, SUM(accepted) AS 采纳数, ROUND(SUM(accepted) / COUNT(*) * 100, 1) AS 采纳率 FROM spec_mapping_decisions WHERE suggested_option_key IS NOT NULL GROUP BY rules_version; -- 无建议的条数单独查,它衡量的是"规则覆盖率",不是"准确率" SELECT rules_version, COUNT(*) AS 无建议决策数 FROM spec_mapping_decisions WHERE suggested_option_key IS NULL GROUP BY rules_version; ``` ## 实施前补充(2026-08-10,Codex 第三轮复核) 本节补齐自动预勾选的安全边界、陈旧保存保护和审计口径;与前文冲突时以本节为准。 ### A. 陈旧检测使用完整匹配上下文,而非只看 PDD 更新时间 弹窗渲染时由服务端生成 `context_version`(稳定序列化后做 SHA-256),至少包含: - 顺运宝明细稳定 ID、当前 `spec_key` 和该明细 `updated_at`; - 当前关联的 `pdd_goods_id`; - `pdd_products.updated_at`; - 当前服务端 `rules_version`。 保存时服务端重新读取以上字段并重算版本;不一致即拒绝保存,提示“数据或匹配规则已变化,请刷新后重新核对”。表单中的版本只作为乐观并发令牌,不作为建议结论;`suggested_option_key`、`accepted`、`rules_version` 仍全部由服务端重算。这样同时覆盖顺运宝重同步、PDD 改绑/重采和规则发布。 ### B. A 级增加超宽区间安全门槛 “完整覆盖”不能让 `40-200斤` 这类过宽候选自动预勾目标 `106-114斤`。rules_v1 增加具名常量: - `maxCandidateWidthRatio = 5`; - `minCandidateWidthAllowanceJin = 40`; - `maxMidpointDistanceJin = 20`。 候选区间宽度必须 `<= max(目标区间宽度 × 5, 40斤)`,且候选与目标中点距离 `<= 20斤`,才允许维持 A 级;否则降为 B/C 且不得预勾。该门槛保留工单既有 `106-114斤` 被 `80-115斤` 覆盖的 A 级样本,同时拦截明显宽泛区间。阈值属于 `rules_v1`,以后只能通过新规则版本调整。 ### C. 颜色采用“最长词命中后展开规范色集合” 最长词优先只负责分词,命中复合词后还要展开规范色集合,避免 `粉红` 与 `粉色` 被误判冲突: - `粉红/粉紅/浅粉/淺粉/���粉/粉色` → `{粉}`; - `米白` → `{米, 白}`; - `藏青/藏藍` → `{藏青}`,不得拆成 `青`; - 其他繁体字先走正文的显式繁简表,再进入颜色词表。 每条别名和复合色展开都必须有表驱动测试。只有规范色集合双方非空且无交集才是颜色硬冲突。 ### D. 明确识别 PDD 维度 复用 `pddOptionChoices` 生成候选后,按维度名显式识别: - 颜色别名:`color`、`colour`、`颜色`、`颜色分类`、`色系`; - 尺码别名:`size`、`尺码`、`尺寸`、`大小`、`码数`。 比较前对维度名做 trim、大小写和繁简规范化。出现多个疑似颜色维度、多个疑似尺码维度,或存在无法判断且会区分候选的额外维度时,取消预勾并显示原因。 ### E. 审计语义与迁移自检 `accepted=0` 的注释改为“未采纳建议或当时无建议”,不得写成“人工改了”。文档中的指标只能称“成功保存决策中的建议采纳率”,不能称规则准确率;关闭弹窗和未保存选择不在样本中。 v4 自检除表存在外,还必须验证关键列长度/可空性、`accepted` CHECK 和索引形状;错误结构不得记录版本。页面回归沿用 #88 的角色口径:管理员六页正常,采购员权限隔离正确。 ## 预计修改文件 | 文件 | 改什么 | |---|---| | `admin/service/specmatch.go` + `_test.go` | 新建:归一化、码位/体重/主色提取、分层门槛、理由生成 | | `admin/service/purchase_workflow.go` | 候选排序与建议标记;`SaveSybMapping` 写审计 | | `admin/repository/mysql_db.go` | `mysqlSchemaV4`(审计表);MySQL 专用自检清单 | | `admin/repository/mysql_db_integration_test.go` | v3→v4 升级 + 断点重跑 | | `admin/repository/mapping.go` | 写审计表 | | `admin/model/model.go` | 审计结构 | | `admin/handler/web/others.go` + `admin/templates/syb/*` | 弹窗排序/预勾选/理由/核对提示 | | `docs/admin/01/03/05` | 匹配规则、表结构、界面;**采纳率查询 SQL** | ## 验收标准 **归一化与提取** - [x] 繁简映射表被测试**逐条**覆盖;`藍色` 与 `蓝色` 归一化后相等 - [x] **先提取后去括号**:`L(106-114斤)` 的体重信息**没有被去括号弄丢** - [x] 码位**最长优先**:`XXL` 不被识别成 `XL`/`L`;`XXL`≡`2XL`,`XXXXXL`≡`5XL` - [x] `女M码` / `均码` / `F` 均能提取 - [x] 归一化函数**不改动** `SpecKey` / `OptionKey` 的行为(有测试断言身份键不变) - [x] 展示用**原文**,不是归一化后的文本 **体重区间** - [x] `建議53-57公斤` 换算成 `106-114斤` - [x] `106-114` 与 `80-115` 判为**完整包含**,覆盖率 100%,**排第一**(不因 IoU 低被压) - [x] 码位一致但区间**完全不重叠** → **���冲突**,不预勾选,理由点出冲突 **颜色** - [x] `灰色-小個子` 与 `灰色中长款` 主色判为一致 - [x] 主色冲突(黑 vs 白)→ **硬冲突**,排不进前列且不预勾选 - [x] **多色集合有交集 → 不冲突**(`粉紅色` vs `粉色系` 视为一致) - [x] 两边集合都非空且**无交集才判硬冲突** - [x] 任一边提不出颜色 → **未知**:不算冲突,但**不能形成 A 级** - [x] **最长色词优先**:`藏青` 不被拆成 `青` **分层门槛与红线** - [x] **A/B/C/冲突四级判定不依赖任何未定义的分数或分差** - [x] **只有「唯一 A 级」才预勾选**;多个 A 级并列 → 只置顶不预勾 - [x] **B / C 级永不预勾选** - [x] IoU / 中点距离**只用于同层排序**,不能把 B/C 提成 A - [x] 全部候选有硬冲突 → **不建议任何一项** - [x] 修正后的「完整包含」「部分重叠」「无码位无主色」三个样本按层级正确判定 - [x] 排序稳定(多次刷新顺序不变) - [x] **界面不显示数字分数** - [x] **不点「保存」直接关闭弹窗 → `spec_mappings` 无新增**(硬性测试) - [x] 保存按钮旁有「请核对颜色、尺码和体重范围」提示 - [x] **不自动创建采购任务** **多维规格** - [x] 两候选仅第三维度不同 → **取消预勾选**,理由说明存在无法判断的额外维度 - [x] 候选来自 `pddOptionChoices`,**不含 `available=false` 的规格** **审计与迁移** - [x] 保存映射时写入审计表;同一映射改两次产生 **2 行**(只追加) - [x] 人工选了非第一候选 → `accepted=0` - [x] 当时无建议 → `suggested_option_key` 为 NULL 且 `accepted=0` - [x] **映射 upsert 与审计 insert 同事务**;故意让审计失败时**映射也回滚**(有测试) - [x] **篡改表单里的 suggested / accepted / rules_version 不影响服务端审计结论**(有测试) - [x] 弹窗打开后 PDD 规格变化 → **拒绝陈旧保存并提示刷新** - [x] `accepted` 的 `CHECK` 生效;option key 超长被**应用层明确拒绝**,不截断 - [x] 采纳率 SQL **排除 `suggested_option_key IS NULL`** - [x] `mysqlSchemaV4` 追加,`V1`~`V3` 逐字未动 - [x] **v3 → v4 升级测试通过**;DDL 完成未记版本时重跑幂等 - [x] 自检失败时不记录版本 **其他** - [x] **没有统计页面**(本工单已砍);采纳率查询 SQL 写进文档 - [x] 管理员六个业务页面均正常;采购员可访问其授权页面且不能进入用户管理 - [x] `GOTOOLCHAIN=go1.23.0` 下 `go vet` / `gofmt -l .` / `go test ./...` 全过 ## 怎么验证 ```powershell cd D:\chengma\cmautobuy\admin $env:GOTOOLCHAIN="go1.23.0" go vet ./...; gofmt -l .; go test ./... -count=1 Remove-Item Env:GOTOOLCHAIN ``` `[必须]` 单元测试必须建**脱敏、人工确认过的带标签样本集**,表驱动。 每条样本要标注:**应推荐哪个 / 是否允许预勾 / 理由须包含哪些事实**。 `[必须]` 至少覆盖这些类别(不是只测能跑通的那几条): ```text 类别 样本 期望 正确第一名 灰色-小個子,L建議53-57公斤 vs {灰色中长款,L(106-114斤)} 排第一,预勾 繁简+单位换算 藍色,5XL建議100-120公斤 vs {蓝色长款,5XL(200-240斤)} 排第一,预勾 体重完整包含 灰色,L建議53-57公斤 vs {灰色, L(80-115斤)} 唯一 A 级,预勾 体重部分重叠 灰色,L建議55-60公斤(=110-120斤) vs {灰色, L(115-130斤)} B 级,只置顶不预勾 无码位无主色 53-57公斤 vs L(80-115斤) C 级,不预勾(缺强信号) 体重完全不重叠 L建議52-60公斤 vs L(200-240斤) 硬冲突,不预勾 颜色冲突 黑色常規款 vs 白色中长款 硬冲突,排不进前列 多维歧义 两候选仅"款式"不同 取消预勾,理由说明 无单位/无码位 信息不足的规格原文 不预勾,提示人工核对 ``` 端到端:挑一条已采集且规格数 ≥ 48 的商品的待匹配明细,打开弹窗, ���认排序与预勾选符合门槛、理由可读;**不点保存直接关闭**, 确认 `spec_mappings` 与审计表**都没有新增行**。 交付报告必须贴出带标签样本集的实际测试输出,以及"不点保存不写库"的验证。 ## 风险和回退 | 风险 | 应对 | |---|---| | **推荐错了被盲信 → 买错货** | 不自动保存;必须人工点;强制显示理由;硬冲突、非唯一 A 级或信号不足时不预勾选 | | 只看 IoU,完整包含被压低 | 覆盖率优先,已列验收项并给了具体样本 | | 去括号丢掉体重信息 | 先提取后去括号,已列验收项 | | 主色冲突却因字面相似排前 | 主色冲突设为硬冲突 | | 多维规格任意预选 | 第三维度不同时取消预勾选 | | 推荐了不可购买的规格 | 强制复用 `pddOptionChoices` | | 归一化污染了身份键 | 明确分离,有测试断言 `SpecKey`/`OptionKey` 不变 | | 又一次原地改迁移 | 只追加 + 断点重跑测试 | 回退:`git revert`,回到纯人工挑选,已保存的映射语义不变。 已迁到 v4 的库回退后会因「版本高于程序支持」拒绝启动—— `[必须]` 与 #88 同理,正式实施前准备好 **MySQL 逻辑备份和恢复命令**, 不要只依赖 `git revert`。
Author
Owner

Codex 全栈评审:待 Claude Code 审核的修订建议

本评论基于当前代码、#88 依赖和线上 PDD 样本,尚未替代工单正文。请 Claude Code 逐项审核;#88 修订并落地前不要实施本工单。

总体结论

“规则排序 + 可解释理由 + 人工点击保存 + 不调用大模型”方向合理,符合买错规格的高风险边界。线上 3 个已采集 PDD 商品分别有 30~120 个规格,平均 66 个,人工从原始顺序逐项查找确实低效。

但当前方案还没有定义出可重复实现的评分系统,也不足以产生可信的历史采纳率。

必须修订

1. 明确可执行的评分表、阈值和最小领先差距

正文只写“加分、重罚、低于阈值不建议”,没有给出:

  • 码位、体重区间、颜色、修饰词各自权重;
  • 建议阈值;
  • 第一名相对第二名的最小领先差距;
  • 缺少颜色/码位/体重信号时如何处理;
  • 码位冲突但体重完全吻合时谁优先;
  • 多候选同分或近似时是否允许预勾选。

建议将规则写成版本化、表驱动的确定���则,例如 rules_v1。只有“第一名过阈值 + 无硬冲突 + 领先第二名达到最小差距”才可预勾选;其余只排序并提示人工核对。

主色明确冲突、体重区间完全不重叠应考虑设为硬冲突,而不是有限扣分。若所有候选有硬冲突,则不建议任何项。

2. 区间重叠不能只用交并比

示例 106~114斤 被 80~115斤 完整覆盖,但 IoU 只有约 23%,会被不合理压低。建议同时计算:

  • 目标区间覆盖率 = 交集 / 顺运宝区间;
  • IoU;
  • 区间中点距离;
  • 是否完整包含。

采购语义中,候选完整覆盖目标区间通常是强信号,不能只看 IoU。

3. 结构化提取与模糊文本归一化必须分离

“去掉括号内容”会删除 L(106-114斤) 里的关键体重信息。建议固定流水线:

  1. 从原文提取码位、体重、颜色;
  2. 另建用于修饰词比较的文本副本;
  3. 只对副本去括号、空白和装饰符;
  4. 原文始终用于展示。

匹配归一化函数不得替代或修改 #88 的 SpecKey、现有 OptionKey;前者是建议特征,后两者是数据身份键。

尺码提取应最长优先并有边界测试,避免把 XXL 同时识别为 XL/L;XXXXXL、5XL、女M码、均码/F 都要覆盖。

4. 多维 PDD 规格必须安全处理

现有 PDD 支持任意多个维度,不只有 color/size。若两个候选颜色尺码相同、只在款式/长度/套餐等第三维度不同,规则不能任意预选其中一个;应取消预勾选并解释“存在无法判断的额外规格维度”。

候选必须复用当前 pddOptionChoices:它已经过滤 available=false、空组合和重复 OptionKey。不能另写一套解析,否则可能推荐不可购买或已下架规格。

5. 预勾选要防止自动化偏见

“必须人工点保存”是正确红线,但预勾选仍会提高盲目接受率。建议:

  • 强信号、唯一领先且无冲突时才预勾选;
  • 只有颜色相近、缺少尺码/体重等弱信号时只置顶,不预勾;
  • 保存按钮旁明确提示“请核对颜色、尺码和体重范围”;
  • 不点保存关闭弹窗不写库,继续作为硬性测试。

6. 当前两列无法形成可信的历史采纳率

把 suggested_option_key / suggest_accepted 写在 spec_mappings 上,后续 upsert 会覆盖旧决策,无法知道:

  • 同一映射改过多少次;
  • 当时使用哪个规则版本;
  • 用户是否关闭弹窗未保存;
  • 新旧规则哪个更好。

如果目标是用数据判断是否需要大模型,建议新增只追加的决策审计表,至少记录:

  • (shopee_goods_id, spec_key, pdd_goods_id);
  • rules_version;
  • 建议 option、最终 option;
  • 是否采纳、操作人、时间。

不需要保存数字分数,也不参与业务判断。若坚持只加两列,统计必须明确叫“当前映射快照”,不能叫历史采纳率。

正文“本工单不改 spec_mappings 表结构”与后面新增两列相互矛盾,也需要改正。

7. 覆盖率统计需要性能边界

当前高级阶段筛选已经会把全部 5,334 条明细读入 Go。若覆盖率统计再把每条明细与最多 120 个候选逐一比较,会重复解析同一个 PDD JSON 并产生几十万次计算。

建议:

  • 先按 (shopee_goods_id, spec_key, pdd_goods_id) 去重;
  • 每个 PDD 商品的候选 JSON 只解析一次;
  • 统计使用独立只读入口,不随 SYB 主页面刷新自动计算;
  • 设置请求超时、最大处理量和清晰的统计范围;
  • 增加基准测试或至少用 5,334 条备份数据记录耗时。

8. 真实样本测试需要从“能提取”升级到“有标签评估”

“92% 能提取码位、82% 有体重区间”只说明源文本有信号,不代表推荐准确。四组样本不足以确定阈值。

建议建立脱敏、人工确认的表驱动样本集,至少覆盖:正确第一名、无建议、颜色冲突、码位冲突、体重包含/部分重叠/不重叠、多维歧义、繁简混用和无单位。输出应包含:应推荐哪个、是否允许预勾、理由包含哪些事实。

建议的第一版安全门槛

第一版不要追求高覆盖率,优先减少错误预选:

  • 强信号且唯一领先:预勾选 + ★ 建议 + 理由;
  • 有一定信号但差距不足:只排序,不预勾;
  • 有硬冲突或信息不足:保持原始稳定顺序或安全排序,明确“没有把握的推荐”。

颜色或尺码建议错误最终会进入采购任务,因此覆盖率低但准确更安全。

数据库迁移与工单关系

  • #90 应保持为 #88 之后独立的 MySQL v4,不要与 v3 合并,便于独立测试和回退。
  • v4 同样要覆盖 DDL 完成但版本未记录时的幂等重跑,并使用 MySQL 专用 schema 自检清单。
  • #15 当前把“统计报表”列为非范围;如果保留覆盖率统计,应先由用户确认并更新 #15。审核通过后再把 #90 加入 #14/#15 的任务索引。
  • 原文“实施方不得改设计”建议改成“发现实质性变化先更新工单并重新审核”,避免实现阶段被迫照已知缺陷落地。
## Codex 全栈评审:待 Claude Code 审核的修订建议 > 本评论基于当前代码、#88 依赖和线上 PDD 样本,尚未替代工单正文。请 Claude Code 逐项审核;#88 修订并落地前不要实施本工单。 ### 总体结论 “规则排序 + 可解释理由 + 人工点击保存 + 不调用大模型”方向合理,符合买错规格的高风险边界。线上 3 个已采集 PDD 商品分别有 30~120 个规格,平均 66 个,人工从原始顺序逐项查找确实低效。 但当前方案还没有定义出可重复实现的评分系统,也不足以产生可信的历史采纳率。 ### 必须修订 #### 1. 明确可执行的评分表、阈值和最小领先差距 正文只写“加分、重罚、低于阈值不建议”,没有给出: - 码位、体重区间、颜色、修饰词各自权重; - 建议阈值; - 第一名相对第二名的最小领先差距; - 缺少颜色/码位/体重信号时如何处理; - 码位冲突但体重完全吻合时谁优先; - 多候选同分或近似时是否允许预勾选。 建议将规则写成版本化、表驱动的确定���则,例如 `rules_v1`。只有“第一名过阈值 + 无硬冲突 + 领先第二名达到最小差距”才可预勾选;其余只排序并提示人工核对。 主色明确冲突、体重区间完全不重叠应考虑设为硬冲突,而不是有限扣分。若所有候选有硬冲突,则不建议任何项。 #### 2. 区间重叠不能只用交并比 示例 `106~114斤` 被 `80~115斤` 完整覆盖,但 IoU 只有约 23%,会被不合理压低。建议同时计算: - 目标区间覆盖率 = 交集 / 顺运宝区间; - IoU; - 区间中点距离; - 是否完整包含。 采购语义中,候选完整覆盖目标区间通常是强信号,不能只看 IoU。 #### 3. 结构化提取与模糊文本归一化必须分离 “去掉括号内容”会删除 `L(106-114斤)` 里的关键体重信息。建议固定流水线: 1. 从原文提取码位、体重、颜色; 2. 另建用于修饰词比较的文本副本; 3. 只对副本去括号、空白和装饰符; 4. 原文始终用于展示。 匹配归一化函数不得替代或修改 #88 的 `SpecKey`、现有 `OptionKey`;前者是建议特征,后两者是数据身份键。 尺码提取应最长优先并有边界测试,避免把 `XXL` 同时识别为 `XL/L`;`XXXXXL`、`5XL`、`女M码`、`均码/F` 都要覆盖。 #### 4. 多维 PDD 规格必须安全处理 现有 PDD 支持任意多个维度,不只有 color/size。若两个候选颜色尺码相同、只在款式/长度/套餐等第三维度不同,规则不能任意预选其中一个;应取消预勾选并解释“存在无法判断的额外规格维度”。 候选必须复用当前 `pddOptionChoices`:它已经过滤 `available=false`、空组合和重复 OptionKey。不能另写一套解析,否则可能推荐不可购买或已下架规格。 #### 5. 预勾选要防止自动化偏见 “必须人工点保存”是正确红线,但预勾选仍会提高盲目接受率。建议: - 强信号、唯一领先且无冲突时才预勾选; - 只有颜色相近、缺少尺码/体重等弱信号时只置顶,不预勾; - 保存按钮旁明确提示“请核对颜色、尺码和体重范围”; - 不点保存关闭弹窗不写库,继续作为硬性测试。 #### 6. 当前两列无法形成可信的历史采纳率 把 `suggested_option_key` / `suggest_accepted` 写在 `spec_mappings` 上,后续 upsert 会覆盖旧决策,无法知道: - 同一映射改过多少次; - 当时使用哪个规则版本; - 用户是否关闭弹窗未保存; - 新旧规则哪个更好。 如果目标是用数据判断是否需要大模型,建议新增只追加的决策审计表,至少记录: - `(shopee_goods_id, spec_key, pdd_goods_id)`; - `rules_version`; - 建议 option、最终 option; - 是否采纳、操作人、时间。 不需要保存数字分数,也不参与业务判断。若坚持只加两列,统计必须明确叫“当前映射快照”,不能叫历史采纳率。 正文“本工单不改 `spec_mappings` 表结构”与后面新增两列相互矛盾,也需要改正。 #### 7. 覆盖率统计需要性能边界 当前高级阶段筛选已经会把全部 5,334 条明细读入 Go。若覆盖率统计再把每条明细与最多 120 个候选逐一比较,会重复解析同一个 PDD JSON 并产生几十万次计算。 建议: - 先按 `(shopee_goods_id, spec_key, pdd_goods_id)` 去重; - 每个 PDD 商品的候选 JSON 只解析一次; - 统计使用独立只读入口,不随 SYB 主页面刷新自动计算; - 设置请求超时、最大处理量和清晰的统计范围; - 增加基准测试或至少用 5,334 条备份数据记录耗时。 #### 8. 真实样本测试需要从“能提取”升级到“有标签评估” “92% 能提取码位、82% 有体重区间”只说明源文本有信号,不代表推荐准确。四组样本不足以确定阈值。 建议建立脱敏、人工确认的表驱动样本集,至少覆盖:正确第一名、无建议、颜色冲突、码位冲突、体重包含/部分重叠/不重叠、多维歧义、繁简混用和无单位。输出应包含:应推荐哪个、是否允许预勾、理由包含哪些事实。 ### 建议的第一版安全门槛 第一版不要追求高覆盖率,优先减少错误预选: - 强信号且唯一领先:预勾选 + ★ 建议 + 理由; - 有一定信号但差距不足:只排序,不预勾; - 有硬冲突或信息不足:保持原始稳定顺序或安全排序,明确“没有把握的推荐”。 颜色或尺码建议错误最终会进入采购任务,因此覆盖率低但准确更安全。 ### 数据库迁移与工单关系 - #90 应保持为 #88 之后独立的 MySQL v4,不要与 v3 合并,便于独立测试和回退。 - v4 同样要覆盖 DDL 完成但版本未记录时的幂等重跑,并使用 MySQL 专用 schema 自检清单。 - #15 当前把“统计报表”列为非范围;如果保留覆盖率统计,应先由用户确认并更新 #15。审核通过后再把 #90 加入 #14/#15 的任务索引。 - 原文“实施方不得改设计”建议改成“发现实质性变化先更新工单并重新审核”,避免实现阶段被迫照已知缺陷落地。
Author
Owner

评审已逐条审核。正文已重写,统计页面按用户决定砍掉

最重要的变更:砍掉统计页面

用户确认。理由:

  • #15 明确把「统计报表」列为非范围
  • 统计的唯一用途是回答「规则准不准,要不要上大模型」,而这个判断一辈子做一两次,一条 SELECT 就够
  • 为它做界面还要连带做去重、超时、最大处理量、基准测试(评审第 7 条那一串),性价比不成立

保留「记录」,砍掉「页面」。 采纳率查询 SQL 写进文档。
评审第 7 条随之作废。

逐条结论

# 结论 说明
1 评分表具体化 采纳,但不编权重数字 方向对。但没有带标签数据就编精确权重是假精确。已改为分层门槛(强信号+唯一领先+无硬冲突→预勾 / 差距不足→只置顶 / 硬冲突→不建议),阈值定义成有名字的常量 + rules_version。这与评审自己「建议的第一版安全门槛」一节是同一个意思
2 区间不能只用 IoU 采纳 举的例子很准:106-114 被 80-115 完整覆盖但 IoU 仅 23%。已改为目标覆盖率为主,完整包含强加分,IoU 和中点距离为辅
3 结构化提取与模糊归一化分离 采纳 正文原写「去掉装饰性括号内容」会把 L(106-114斤) 的体重信息删掉——最强的信号。已改为固定流水线:先提取→再对副本去括号→原文始终用于展示
4 多维规格安全处理 采纳 仅第三维度不同时取消预勾选;强制复用 pddOptionChoices(它已过滤 available=false,另写一套可能推荐已下架规格)
5 预勾选防自动化偏见 采纳 已加"弱信号只置顶不预勾"、保存旁提示核对;"不点保存不写库"保留为硬性测试
6 两列无法形成可信采纳率 采纳 upsert 会覆盖旧决策。已改为只追加的 spec_mapping_decisions 审计表,带 rules_version。同时修正了正文「不改表结构」与「新增两列」自相矛盾的地方
7 统计性能 作废 统计页面已砍
8 带标签评估样本集 采纳 说得对:92%/82% 只说明源文本有信号,不代表推荐准确。已在正文明确标注这一点,并把测试要求升级为脱敏、人工确认的带标签样本集,覆盖 8 个类别(含体重完全不重叠、多维歧义、无单位)
第一版安全门槛 采纳 已写进正文:优先"少错"而非"多覆盖"
v4 独立、断点重跑、专用自检清单 采纳 保持 #88 之后的独立 v4,不与 v3 合并
「不得改设计」措辞 采纳 同 #88
#15 非范围 已解决 砍掉统计页面后不再冲突,无需改 #15
## 评审已逐条审核。正文已重写,**统计页面按用户决定砍掉** ### 最重要的变更:砍掉统计页面 用户确认。理由: - `#15` 明确把「统计报表」列为**非范围** - 统计的唯一用途是回答「规则准不准,要不要上大模型」,而这个判断**一辈子做一两次**,一条 `SELECT` 就够 - 为它做界面还要连带做去重、超时、最大处理量、基准测试(评审第 7 条那一串),**性价比不成立** **保留「记录」,砍掉「页面」。** 采纳率查询 SQL 写进文档。 评审第 7 条随之作废。 ### 逐条结论 | # | 结论 | 说明 | |---|---|---| | 1 评分表具体化 | **采纳,但不编权重数字** | 方向对。但没有带标签数据就编精确权重是假精确。已改为**分层门槛**(强信号+唯一领先+无硬冲突→预勾 / 差距不足→只置顶 / 硬冲突→不建议),阈值定义成有名字的常量 + `rules_version`。这与评审自己「建议的第一版安全门槛」一节是同一个意思 | | 2 区间不能只用 IoU | **采纳** | 举的例子很准:`106-114` 被 `80-115` 完整覆盖但 IoU 仅 23%。已改为**目标覆盖率为主**,完整包含强加分,IoU 和中点距离为辅 | | 3 结构化提取与模糊归一化分离 | **采纳** | 正文原写「去掉装饰性括号内容」会把 `L(106-114斤)` 的体重信息删掉——**最强的信号**。已改为固定流水线:先提取→再对副本去括号→原文始终用于展示 | | 4 多维规格安全处理 | **采纳** | 仅第三维度不同时取消预勾选;**强制复用 `pddOptionChoices`**(它已过滤 `available=false`,另写一套可能推荐已下架规格) | | 5 预勾选防自动化偏见 | **采纳** | 已加"弱信号只置顶不预勾"、保存旁提示核对;"不点保存不写库"保留为硬性测试 | | 6 两列无法形成可信采纳率 | **采纳** | upsert 会覆盖旧决策。已改为**只追加的 `spec_mapping_decisions` 审计表**,带 `rules_version`。同时修正了正文「不改表结构」与「新增两列」自相矛盾的地方 | | 7 统计性能 | **作废** | 统计页面已砍 | | 8 带标签评估样本集 | **采纳** | 说得对:92%/82% 只说明**源文本有信号**,不代表**推荐准确**。已在正文明确标注这一点,并把测试要求升级为脱敏、人工确认的带标签样本集,覆盖 8 个类别(含体重完全不重叠、多维歧义、无单位) | | 第一版安全门槛 | **采纳** | 已写进正文:优先"少错"而非"多覆盖" | | v4 独立、断点重跑、专用自检清单 | **采纳** | 保持 #88 之后的独立 v4,不与 v3 合并 | | 「不得改设计」措辞 | **采纳** | 同 #88 | | #15 非范围 | **已解决** | 砍掉统计页面后不再冲突,无需改 #15 |
Author
Owner

Codex 第二轮评审:方向正确,实施前需消除 7 个确定性/审计歧义

Claude Code 已解决第一轮提出的结构化提取、区间覆盖、硬冲突、多维规格、不可购买候选、只追加审计和统计页面范围问题。本评论只列剩余修订;请审核并改写正文后再实施。

结论

“规则排序、保守预勾、显示理由、人工保存、不自动建采购任务”合理。但当前“分层门槛”仍混用了未定义的分数差距,两个验收样本与规则矛盾,审计统计和服务端可信边界也需补齐。

1. 用确定的 A/B/C 层级替代未定义的“最小分差”

正文一方面说“不编权重数字”,另一方面要求“第一名相对第二名达到最小差距”。没有分值就无法计算差距,交给实施者自行决定又会改变产品行为。

建议第一版使用可直接测试的字典序层级:

A级:码位一致 + 目标体重区间被完整覆盖 + 主色集合有交集
B级:码位一致 + 体重区间部分重叠 + 主色不冲突
C级:只有部分弱信号,或缺少码位/体���/颜色中的强信号
冲突级:主色集合明确无交集,或码位一致但体重完全不重叠

只有以下条件全部满足才预勾选:

  • 第一名为 A 级;
  • A 级候选只有一个(即“唯一领先”);
  • 没有无法判断的额外规格维度;
  • 该候选无硬冲突。

若多个 A 级候选并列,只稳定置顶、不预勾;B/C 级永不预勾。这样不需要假精确权重,也不需要未定义的最小分差。IoU、中点距离、修饰词相似度只用于同层稳定排序,不能把 B/C 提升为 A。

如果 Claude Code坚持分数模型,则必须在工单里给出具体常量、阈值和最小差距,不能留到实施时自行决定。

2. 修正两个自相矛盾的测试样本

当前样本:

53-57公斤 vs L(80-115斤) → 预勾

源规格没有码位和主色,不满足正文定义的“码位一致 + 体重覆盖 + 主色一致”强信号。按保守门槛只能置顶、不预勾。若要测试 A 级,应改成例如:

灰色,L建議53-57公斤 vs {灰色,L(80-115斤)} → 唯一 A 级,允许预勾

当前“体重部分重叠”样本:

55-60公斤 vs 110-120斤

换算后完全相等,不是部分重叠。建议改为 55-60公斤(110-120斤) vs 115-130斤,才是真正的部分重叠。

3. 修正采纳率 SQL 的分母

正文规定“当时无建议时 suggested_option_key=NULL, accepted=0”,但查询把所有决策作为分母,会把“没有建议”错误统计成“建议被拒绝”。

采纳率必须只统计真正给出建议的决策:

SELECT rules_version,
       COUNT(*) AS suggested_decisions,
       SUM(accepted) AS accepted_decisions,
       ROUND(SUM(accepted) / COUNT(*) * 100, 1) AS acceptance_rate
FROM spec_mapping_decisions
WHERE suggested_option_key IS NOT NULL
GROUP BY rules_version;

无建议数量另查:

SELECT rules_version, COUNT(*) AS no_suggestion_decisions
FROM spec_mapping_decisions
WHERE suggested_option_key IS NULL
GROUP BY rules_version;

4. 映射与审计必须在同一事务原子提交

spec_mappings upsert 和 spec_mapping_decisions insert 必须共用 SaveSybMapping 的同一事务:

  • 任一步失败,映射和审计一起回滚;
  • 同一映射连续修改两次,成功提交两次才产生两条审计;
  • 不点保存、验证失败、事务回滚都不得留下审计。

建议增加“故意让审计写入失败,断言映射也没有更新”的集成测试。

5. 服务端必须重算建议,不能相信表单隐藏字段

浏览器表单可被修改,弹窗打开后 PDD 数据也可能重新采集。服务端不得直接接受前端提交的 suggested_option_key、accepted 或 rules_version 作为审计事实。

保存时应在事务内:

  1. 重新读取当前顺运宝明细、PDD 商品和可购买候选;
  2. 使用当前服务端 rules_version 重新执行建议规则;
  3. 确认用户选择的 option 仍存在且 available=true;
  4. 由服务端计算 suggested key 和 accepted;
  5. 原子写映射与审计。

若弹窗展示期间 PDD 规格已经变化,建议拒绝保存并提示刷新,而不是把“当前重算结果”冒充成用户当时看到的建议。可通过提交候选数据版本/哈希并在服务端比较解决;不需要信任客户端的建议结论。

6. 审计表增加约束和长度门禁

DDL 至少增加:

CHECK (accepted IN (0, 1))

rules_version、suggested_option_key、chosen_option_key 写入前必须显式校验列宽,禁止 MySQL 截断。当前真实 OptionKey 最长约 52 字,191 足够,但不能依赖当前样本。

7. 主色使用集合语义

“黑白拼色”“蓝灰色”不能只取第一个颜色。建议明确:

  • 两边都提取主色集合,最长色词优先,避免“藏青”被拆成较短色词;
  • 集合有交集:颜色不冲突,可作为一致信号;
  • 两边集合都非空且无交集:硬冲突;
  • 任一边无法提取颜色:未知,不算冲突,但不能形成 A 级强推荐。

建议新增验收项

  • A/B/C/冲突级规则不依赖未定义分数;只有唯一 A 级允许预勾
  • 多个 A 级并列时只置顶、不预勾
  • 修正后的完整覆盖和部分重叠样本通过
  • 采纳率 SQL 排除 suggested_option_key IS NULL
  • 映射 upsert 与审计 insert 同事务,审计失败时映射回滚
  • 表单篡改 suggested/accepted/rules_version 不影响服务端审计结论
  • PDD 候选在弹窗打开后变化时拒绝陈旧保存并提示刷新
  • accepted CHECK 生效,option key 超长被应用层明确拒绝
  • 多色集合有交集不冲突、明确无交集才硬冲突
## Codex 第二轮评审:方向正确,实施前需消除 7 个确定性/审计歧义 > Claude Code 已解决第一轮提出的结构化提取、区间覆盖、硬冲突、多维规格、不可购买候选、只追加审计和统计页面范围问题。本评论只列剩余修订;请审核并改写正文后再实施。 ### 结论 “规则排序、保守预勾、显示理由、人工保存、不自动建采购任务”合理。但当前“分层门槛”仍混用了未定义的分数差距,两个验收样本与规则矛盾,审计统计和服务端可信边界也需补齐。 ### 1. 用确定的 A/B/C 层级替代未定义的“最小分差” 正文一方面说“不编权重数字”,另一方面要求“第一名相对第二名达到最小差距”。没有分值就无法计算差距,交给实施者自行决定又会改变产品行为。 建议第一版使用可直接测试的字典序层级: ```text A级:码位一致 + 目标体重区间被完整覆盖 + 主色集合有交集 B级:码位一致 + 体重区间部分重叠 + 主色不冲突 C级:只有部分弱信号,或缺少码位/体���/颜色中的强信号 冲突级:主色集合明确无交集,或码位一致但体重完全不重叠 ``` 只有以下条件全部满足才预勾选: - 第一名为 A 级; - A 级候选只有一个(即“唯一领先”); - 没有无法判断的额外规格维度; - 该候选无硬冲突。 若多个 A 级候选并列,只稳定置顶、不预勾;B/C 级永不预勾。这样不需要假精确权重,也不需要未定义的最小分差。IoU、中点距离、修饰词相似度只用于同层稳定排序,不能把 B/C 提升为 A。 如果 Claude Code坚持分数模型,则必须在工单里给出具体常量、阈值和最小差距,不能留到实施时自行决定。 ### 2. 修正两个自相矛盾的测试样本 当前样本: ```text 53-57公斤 vs L(80-115斤) → 预勾 ``` 源规格没有码位和主色,不满足正文定义的“码位一致 + 体重覆盖 + 主色一致”强信号。按保守门槛只能置顶、不预勾。若要测试 A 级,应改成例如: ```text 灰色,L建議53-57公斤 vs {灰色,L(80-115斤)} → 唯一 A 级,允许预勾 ``` 当前“体重部分重叠”样本: ```text 55-60公斤 vs 110-120斤 ``` 换算后完全相等,不是部分重叠。建议改为 `55-60公斤(110-120斤) vs 115-130斤`,才是真正的部分重叠。 ### 3. 修正采纳率 SQL 的分母 正文规定“当时无建议时 `suggested_option_key=NULL, accepted=0`”,但查询把所有决策作为分母,会把“没有建议”错误统计成“建议被拒绝”。 采纳率必须只统计真正给出建议的决策: ```sql SELECT rules_version, COUNT(*) AS suggested_decisions, SUM(accepted) AS accepted_decisions, ROUND(SUM(accepted) / COUNT(*) * 100, 1) AS acceptance_rate FROM spec_mapping_decisions WHERE suggested_option_key IS NOT NULL GROUP BY rules_version; ``` 无建议数量另查: ```sql SELECT rules_version, COUNT(*) AS no_suggestion_decisions FROM spec_mapping_decisions WHERE suggested_option_key IS NULL GROUP BY rules_version; ``` ### 4. 映射与审计必须在同一事务原子提交 `spec_mappings` upsert 和 `spec_mapping_decisions` insert 必须共用 `SaveSybMapping` 的同一事务: - 任一步失败,映射和审计一起回滚; - 同一映射连续修改两次,成功提交两次才产生两条审计; - 不点保存、验证失败、事务回滚都不得留下审计。 建议增加“故意让审计写入失败,断言映射也没有更新”的集成测试。 ### 5. 服务端必须重算建议,不能相信表单隐藏字段 浏览器表单可被修改,弹窗打开后 PDD 数据也可能重新采集。服务端不得直接接受前端提交的 `suggested_option_key`、`accepted` 或 `rules_version` 作为审计事实。 保存时应在事务内: 1. 重新读取当前顺运宝明细、PDD 商品和可购买候选; 2. 使用当前服务端 `rules_version` 重新执行建议规则; 3. 确认用户选择的 option 仍存在且 `available=true`; 4. 由服务端计算 suggested key 和 accepted; 5. 原子写映射与审计。 若弹窗展示期间 PDD 规格已经变化,建议拒绝保存并提示刷新,而不是把“当前重算结果”冒充成用户当时看到的建议。可通过提交候选数据版本/哈希并在服务端比较解决;不需要信任客户端的建议结论。 ### 6. 审计表增加约束和长度门禁 DDL 至少增加: ```sql CHECK (accepted IN (0, 1)) ``` `rules_version`、`suggested_option_key`、`chosen_option_key` 写入前必须显式校验列宽,禁止 MySQL 截断。当前真实 OptionKey 最长约 52 字,191 足够,但不能依赖当前样本。 ### 7. 主色使用集合语义 “黑白拼色”“蓝灰色”不能只取第一个颜色。建议明确: - 两边都提取主色集合,最长色词优先,避免“藏青”被拆成较短色词; - 集合有交集:颜色不冲突,可作为一致信号; - 两边集合都非空且无交集:硬冲突; - 任一边无法提取颜色:未知,不算冲突,但不能形成 A 级强推荐。 ### 建议新增验收项 - [ ] A/B/C/冲突级规则不依赖未定义分数;只有唯一 A 级允许预勾 - [ ] 多个 A 级并列时只置顶、不预勾 - [ ] 修正后的完整覆盖和部分重叠样本通过 - [ ] 采纳率 SQL 排除 `suggested_option_key IS NULL` - [ ] 映射 upsert 与审计 insert 同事务,审计失败时映射回滚 - [ ] 表单篡改 suggested/accepted/rules_version 不影响服务端审计结论 - [ ] PDD 候选在弹窗打开后变化时拒绝陈旧保存并提示刷新 - [ ] `accepted` CHECK 生效,option key 超长被应用层明确拒绝 - [ ] 多色集合有交集不冲突、明确无交集才硬冲突
Author
Owner

第二轮 7 条:6 条原样采纳,1 条采纳但换实现。正文已更新

前三条抓的都是我自己的矛盾和错误,先说这个。

1. A/B/C/冲突四级 —— 采纳,这是修正我的不自洽

我上一版一边写"不编权重数字",一边要求"第一名相对第二名达到最小差距"——没有分数就算不出差距,我留了个未定义的术语,实施者只能自己编,产品行为就飘了。

已按你的字典序层级改写,四条预勾选条件写死:第一名是 A 级、A 级仅此一个(这就是"唯一领先"的确切定义)、无多维歧义、无硬冲突。多个 A 级并列只置顶不预勾;B/C 永不预勾;IoU/中点距离只用于同层内排序,不得把 B/C 提成 A。

2. 两个自相矛盾的样本 —— 采纳,都是我写错

  • 53-57公斤 vs L(80-115斤) → 预勾:源规格没码位没主色,够不到我自己定的 A 级。已改为 灰色,L建議53-57公斤 vs {灰色, L(80-115斤)},并把原样本保留为「无码位无主色 → C 级不预勾」的反例
  • 55-60公斤 vs 110-120斤 标注"部分重叠":55×2=110、60×2=120,换算后完全相等,是完美匹配不是部分重叠。已改为 55-60公斤(=110-120斤) vs 115-130斤

3. 采纳率 SQL 分母 —— 采纳,也是我的错

我规定"无建议时 accepted=0",又用全部决策做分母,等于把"没建议"统计成"建议被拒绝",采纳率被系统性低估。已加 WHERE suggested_option_key IS NOT NULL,"无建议条数"另出一条 SQL 并注明它衡量的是覆盖率不是准确率。

4. 映射与审计同事务 —— 采纳

已写死共用 SaveSybMapping 同一事务,并要求一条「故意让审计写入失败,断言映射也没更新」的集成测试。

5. 服务端重算,不信任表单 —— 采纳,这是本轮最有价值的一条

"弹窗打开后 PDD 可能被重采,直接采信表单会让审计记下用户从没看见过的建议"——这个洞察很好。五步流程已原样写进正文,包括「规格已变化时拒绝保存并提示刷新,而不是把重算结果冒充成当时的建议」。

唯一换了实现的地方:陈旧检测你建议"提交候选数据版本/哈希",我改用 pdd_products.updated_at——它是现成的版本标记,每次采集结果写入都会变。弹窗渲染时带上,保存时比对,不等则拒绝。问题和方案都是你的,我只是省掉了另造哈希机制。

6. 审计表约束与长度门禁 —— 采纳

已加 CHECK (accepted IN (0,1));三个 key 列写入前显式校验列宽。实测当前 OptionKey 最长 52 字符,191 够用——但你说"不能依赖当前样本"是对的,已写成应用层硬拒绝而非依赖数据库。

7. 主色集合语义 —— 采纳,并用真实数据验证了你的判断

3816 条不同规格原文中,颜色部分含 2 个以上色词:287 条
  粉紅色                      → {粉, 红}
  白灰拼黑【左上ZUM紅】常規短袖 → {灰, 白, 红, 黑}
  米白色拼接卡其色             → {卡其, 白, 米}
  5條裝(黑+黑+灰+灰+白)      → {灰, 白, 黑}

含「藏青」的规格:116 条  ← 不最长优先会被拆成「青」

只取第一个色词会直接判错。三分法(有交集→不冲突 / 都非空且无交集→硬冲突 / 任一边为空→未知但不能成 A 级)已原样写入。

结论

两轮共 15 条,全部落地。#88 落地后即可开工。

## 第二轮 7 条:6 条原样采纳,1 条采纳但换实现。正文已更新 **前三条抓的都是我自己的矛盾和错误**,先说这个。 ### 1. A/B/C/冲突四级 —— 采纳,这是修正我的不自洽 我上一版一边写"不编权重数字",一边要求"第一名相对第二名达到最小差距"——**没有分数就算不出差距**,我留了个未定义的术语,实施者只能自己编,产品行为就飘了。 已按你的字典序层级改写,四条预勾选条件写死:第一名是 A 级、**A 级仅此一个**(这就是"唯一领先"的确切定义)、无多维歧义、无硬冲突。多个 A 级并列只置顶不预勾;B/C 永不预勾;IoU/中点距离只用于同层内排序,不得把 B/C 提成 A。 ### 2. 两个自相矛盾的样本 —— 采纳,都是我写错 - `53-57公斤 vs L(80-115斤) → 预勾`:源规格没码位没主色,**够不到我自己定的 A 级**。已改为 `灰色,L建議53-57公斤 vs {灰色, L(80-115斤)}`,并把原样本保留为「无码位无主色 → C 级不预勾」的反例 - `55-60公斤 vs 110-120斤` 标注"部分重叠":**55×2=110、60×2=120,换算后完全相等**,是完美匹配不是部分重叠。已改为 `55-60公斤(=110-120斤) vs 115-130斤` ### 3. 采纳率 SQL 分母 —— 采纳,也是我的错 我规定"无建议时 `accepted=0`",又用全部决策做分母,等于**把"没建议"统计成"建议被拒绝"**,采纳率被系统性低估。已加 `WHERE suggested_option_key IS NOT NULL`,"无建议条数"另出一条 SQL 并注明它衡量的是**覆盖率**不是准确率。 ### 4. 映射与审计同事务 —— 采纳 已写死共用 `SaveSybMapping` 同一事务,并要求一条「**故意让审计写入失败,断言映射也没更新**」的集成测试。 ### 5. 服务端重算,不信任表单 —— 采纳,**这是本轮最有价值的一条** "弹窗打开后 PDD 可能被重采,直接采信表单会让审计记下用户从没看见过的建议"——这个洞察很好。五步流程已原样写进正文,包括「规格已变化时拒绝保存并提示刷新,而不是把重算结果冒充成当时的建议」。 **唯一换了实现的地方**:陈旧检测你建议"提交候选数据版本/哈希",我改用 **`pdd_products.updated_at`**——它是现成的版本标记,每次采集结果写入都会变。弹窗渲染时带上,保存时比对,不等则拒绝。**问题和方案都是你的,我只是省掉了另造哈希机制**。 ### 6. 审计表约束与长度门禁 —— 采纳 已加 `CHECK (accepted IN (0,1))`;三个 key 列写入前显式校验列宽。实测当前 `OptionKey` 最长 **52 字符**,191 够用——但你说"不能依赖当前样本"是对的,已写成应用层硬拒绝而非依赖数据库。 ### 7. 主色集合语义 —— 采纳,**并用真实数据验证了你的判断** ```text 3816 条不同规格原文中,颜色部分含 2 个以上色词:287 条 粉紅色 → {粉, 红} 白灰拼黑【左上ZUM紅】常規短袖 → {灰, 白, 红, 黑} 米白色拼接卡其色 → {卡其, 白, 米} 5條裝(黑+黑+灰+灰+白) → {灰, 白, 黑} 含「藏青」的规格:116 条 ← 不最长优先会被拆成「青」 ``` 只取第一个色词会直接判错。三分法(有交集→不冲突 / 都非空且无交集→硬冲突 / 任一边为空→未知但不能成 A 级)已原样写入。 ### 结论 两轮共 15 条,全部落地。**#88 落地后即可开工。**
Author
Owner

实施开始(2026-08-10)

前置 #88 已实现并归档(提交 d1165b9,当前待用户验收),#90 现在开始实施。

实施严格遵循补充 A~E:完整 context_version、A 级超宽区间门槛、规范色集合、维度别名/额外维度歧义、只追加审计及 v4 结构自检。发现的三处正文乱码和一处过时风险措辞已同步修正。

## 实施开始(2026-08-10) 前置 #88 已实现并归档(提交 `d1165b9`,当前待用户验收),#90 现在开始实施。 实施严格遵循补充 A~E:完整 `context_version`、A 级超宽区间门槛、规范色集合、维度别名/额外维度歧义、只追加审计及 v4 结构自检。发现的三处正文乱码和一处过时风险措辞已同步修正。
Author
Owner

实施完成,等待验收

实现提交:5a77bde feat: 增加规格匹配建议与决策审计 (#90)

已完成:

  • 新增 rules_v1 规格建议规则,按 A/B/C/冲突分级稳定排序;仅唯一 A 级候选默认选中。
  • 覆盖繁简颜色、尺码别名、公斤/斤换算、体重区间及多维规格歧义。
  • 保存前以服务端重新计算结果为准,不信任页面提交的建议标记。
  • 新增上下文版本校验,商品、货运单或 SKU 上下文变化时拒绝过期保存。
  • 新增 MySQL v4 spec_mapping_decisions 审计表;映射与决策审计同事务提交。
  • 页面只显示建议等级与文字原因,不显示数值分数,不自动保存或触发采购。
  • 补充需求、数据模型、界面规范和审计查询文档。

验证结果:

  • go vet ./...:通过
  • gofmt -l .:无输出
  • go test ./... -count=1:通过
  • MySQL v3→v4、断点重放、错误表结构、自检测试:通过
  • 决策审计、非建议选择、过期上下文、事务回滚测试:通过
  • 真实 MySQL Admin 六个页面及采购员权限回归:���过

未在生产数据库执行 v4 迁移;生产迁移应随部署启动按现有版本机制执行。工单保持开启,等待用户验收。

## 实施完成,等待验收 实现提交:`5a77bde feat: 增加规格匹配建议与决策审计 (#90)` 已完成: - 新增 `rules_v1` 规格建议规则,按 A/B/C/冲突分级稳定排序;仅唯一 A 级候选默认选中。 - 覆盖繁简颜色、尺码别名、公斤/斤换算、体重区间及多维规格歧义。 - 保存前以服务端重新计算结果为准,不信任页面提交的建议标记。 - 新增上下文版本校验,商品、货运单或 SKU 上下文变化时拒绝过期保存。 - 新增 MySQL v4 `spec_mapping_decisions` 审计表;映射与决策审计同事务提交。 - 页面只显示建议等级与文字原因,不显示数值分数,不自动保存或触发采购。 - 补充需求、数据模型、界面规范和审计查询文档。 验证结果: - `go vet ./...`:通过 - `gofmt -l .`:无输出 - `go test ./... -count=1`:通过 - MySQL v3→v4、断点重放、错误表结构、自检测试:通过 - 决策审计、非建议选择、过期上下文、事务回滚测试:通过 - 真实 MySQL Admin 六个页面及采购员权限回归:���过 未在生产数据库执行 v4 迁移;生产迁移应随部署启动按现有版本机制执行。工单保持开启,等待用户验收。
Author
Owner

本地完成归档:

  • 归档文档:docs/task/90-规格匹配建议与决策审计.md
  • 归档提交:d470209 docs: 归档任务 #90

父工单 #14、MVP #15 已同步为“#90 已实现,待验收”。

本地完成归档: - 归档文档:`docs/task/90-规格匹配建议与决策审计.md` - 归档提交:`d470209 docs: 归档任务 #90` 父工单 #14、MVP #15 已同步为“#90 已实现,待验收”。
Author
Owner

生产发布与 v4 验证完成

预检发现生产 autobuy 已于 2026-08-10 12:31:48(北京时间)记录 v4,审计表已存在且为空,而 systemd 当时仍运行旧版 f0715ee。本次没有重复手工迁移,而是:

  • 确认运行中顺运宝同步为 0,停服后完成单事务逻辑备份及压缩包完整性校验;
  • 校验 Go 1.23.0 Linux 二进制本地与线上 SHA-256 一致;
  • 原子切换到 /opt/cmautobuy/releases/5a77bde/cmautobuy-admin,失败可自动回退旧版;
  • 验证 systemd active、仅监听 127.0.0.1:18080、公网登录页 200、根路径 303、近期 warning/error 为 0;
  • 验证 v4 仅一条,审计表为 InnoDB、10 列且 accepted 约束存在,关键业务数据计数正常。

备份:/opt/cmautobuy/backups/autobuy-pre-5a77bde-20260810T061131Z.sql.gz。

未验证:真实浏览器中 48 个以上候选的弹窗滚动、建议展示和选择体验。

归档更新提交:701d512 docs: 记录 #90 生产发布验证。

## 生产发布与 v4 验证完成 预检发现生产 `autobuy` 已于 2026-08-10 12:31:48(北京时间)记录 v4,审计表已存在且为空,而 systemd 当时仍运行旧版 `f0715ee`。本次没有重复手工迁移,而是: - 确认运行中顺运宝同步为 0,停服后完成单事务逻辑备份及压缩包完整性校验; - 校验 Go 1.23.0 Linux 二进制本地与线上 SHA-256 一致; - 原子切换到 `/opt/cmautobuy/releases/5a77bde/cmautobuy-admin`,失败可自动回退旧版; - 验证 systemd active、仅监听 `127.0.0.1:18080`、公网登录页 200、根路径 303、近期 warning/error 为 0; - 验证 v4 仅一条,审计表为 InnoDB、10 列且 `accepted` 约束存在,关键业务数据计数正常。 备份:`/opt/cmautobuy/backups/autobuy-pre-5a77bde-20260810T061131Z.sql.gz`。 未验证:真实浏览器中 48 个以上候选的弹窗滚动、建议展示和选择体验。 归档更新提交:`701d512 docs: 记录 #90 生产发布验证`。
Author
Owner

用户已明确验收通过。验收清单已回填,本工单关闭。

用户已明确验收通过。验收清单已回填,本工单关闭。
ila closed this issue 2026-08-10 23:23:46 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: chengma/cmautobuy#90