# 模块开发进度

> 每次会话结束时更新本文件，下次会话开场先读此文件。
> 状态：✅ 完成 | 🚧 进行中 | ❌ 未开始 | ⚠️ 有问题待处理
>
> **会话记录是两层结构（2026-09-16 起）**：本文件「最近会话记录」只保留表格现状 + 每次会话一行摘要；
> 完整记录（自测明细、踩坑、决策过程）在 [modules.archive.md](modules.archive.md)。
> 日常会话开场**不需要**读 archive；只有排查某次改动的具体原因、找踩坑记录时才按日期/关键词去查。
> 会话结束时：本文件加一行摘要（下面那种格式），完整版追加到 archive 末尾。

## 系统用户

| 模块 | Model | Action | View | 状态 | 备注 |
|------|-------|--------|------|------|------|
| depa（部门） | ✅ | ✅ | ✅ | ✅ 完成 | |
| users（用户） | ✅ | ✅ | ✅ | ✅ 完成 | grid/导出不带密码哈希（2026-09-14），浏览器复验待人工 |
| member（会员） | ✅ | ✅ | ✅ | ✅ 完成 | 同上，`member_password` |

## 权限

| 模块 | Model | Action | View | 状态 | 备注 |
|------|-------|--------|------|------|------|
| site（站点） | ✅ | ✅ | ✅ | ✅ 完成 | |
| role（角色） | ✅ | ✅ | ✅ | ✅ 完成 | |
| permission（权限） | ✅ | ✅ | ✅ | ✅ 完成 | |
| rolePermission（角色与权限） | ✅ | ✅ | ✅ | ✅ 完成 | |
| roleAdmin（角色与系统用户） | ✅ | ✅ | ✅ | ✅ 完成 | |

## 系统

| 模块 | Model | Action | View | 状态 | 备注 |
|------|-------|--------|------|------|------|
| attrType（属性类型） | ✅ | ✅ | ✅ | ✅ 完成 | |
| attrValue（属性值） | ✅ | ✅ | ✅ | ✅ 完成 | |
| gridLayout（表格布局） | ✅ | ✅ | — | ✅ 完成 | 无独立视图 |
| langTool（语言包工具） | — | ✅ | ✅ | ✅ 完成 | 无 Model |

## 数据清单

| 模块 | Model | Action | View | 状态 | 备注 |
|------|-------|--------|------|------|------|
| washSheet（数据单） | ✅ | ✅ | ✅ | ✅ 完成 | 表 `wash_sheet` 已建（2026-09-14 核对实际库）；列表点标题打开单张单的明细标签。2026-09-28 列表加明细统计四列（明细数 / 已清洗 / 有清洗数据 / 零件库存在，后三列比例条），`WashSheet::grid()` 按当前页单号现算，不能筛选排序，无 DDL |
| washSheet/detail（单张单的明细） | — | ✅ | ✅ | ✅ 完成 | 路由 `/washSheet/detail/k/sheetId/{单号}`，数据接口复用 washSheetItem（带 `sheetId` 限定范围）；带「导入数据单」粘贴导入 |
| washSheetItem（数据单明细） | ✅ | ✅ | ✅ | ✅ 完成 | 表 `wash_sheet_item` 已建；明细增删改会同步刷新数据单的更新时间。外键已建（2026-09-14）：数据单级联删明细，厂商被明细使用时不能删，删用户时数据单添加人置 NULL |
| 数据单清洗：确认参考产品 / 匹配分类 / 深度清洗（S6a） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-17：`sql/migrate_wash_confirm_clean.sql`（v1.0.4）经用户授权执行；新增 Model `WashSheetItemClean`，`WashSheetItem` 加确认 / 分类 / 深度清洗；两个明细页新增分类列、深度清洗列、「匹配分类」「修改分类」按钮和「清洗详情」弹窗。CLI 38 项 + 浏览器实测通过，语言包新增 55 条已译。规则见下方「清洗 S6a」和 DB.md |
| 数据单转零件（S6b） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-17：`sql/migrate_wash_to_part.sql`（v1.0.5，明细 2 列 + `part_number_car_brand_text`）经用户授权执行；新 Model `WashSheetItemPart`，端点 `washSheetItem/toPart`，两个明细页工具条「转零件」+「零件」列，零件号码分页加「汽车品牌文本」。CLI 只读 39 + 写入 36 项、浏览器实测通过，测试数据已清，语言包新增 22 条已译。规则见下方「转零件 S6b」和 DB.md |

导入规则（`washSheetItem/import`）：一行一条，`厂商:号码` 为格式2，其余按格式1 当作 OE 号；
厂商必须在「号码厂商」里已登记（`OE` 首次导入时自动补一条），未登记的整行跳过并回报；
按「厂商+号码」在本单内去重，已存在的不重复新增。

数据清洗（`washSheetItem/clean`，逻辑在 `model/Wash.php`）：
两个明细页都有「清洗数据」按钮（多选 + 勾选框常驻），弹窗选清洗方式（仅处理未清洗部分 / 重新清洗），
前端按选中行**逐条**调后端并走进度条——因为 `tcd/checkNumberData` 传多个号码时结果是合并返回的，拆不回单条。
网络类错误（status 0 / 5xx）隔 1 秒重试一次，业务错误不重试；关掉进度弹窗即停止后续行。
链路：TCD = `tcd/cleanData`；EPC = `tcd/checkNumberData` 取 etk_id/pro_id/grp2_id → `etk/cleanData`。
结果只存摘要（`wash_sheet_item_tcd` / `wash_sheet_item_epc`），因为 `tcd/cleanData` 单条响应约 60KB，整包落库会拖垮 grid。

结果列展示（`washClean.js` 的 `washCleanColumns()`）：
- **TCD 列**取 `checkNumberData` 的 `tcd.ga_chk` —— 分类名称 + `[N个]` 号码数，点名称展开该分类下的「厂商: 号码」，
  每行一个、单选（同一行内跨分类只能选一个）。名称中英文都存（`name_cn` / `name_en`），前端按 `APP.LOCALE` 选。
  接口每个分类只回前 10 条明细，`count` 大于明细数时列尾会提示。
- **EPC 列**：零件组/零件名 + OE 号（单选）+ 替换号列表。`renum` 由接口按新→旧排序，**`renum[0]` 打「新」标记**；
  `isnew` 是另一回事（=1 表示被查号码本身就是最新号，实测查 5Q0199868J 得 1、查其旧号 5Q0199868F 得 -1），一并存着备用。
- 号码链接：TCD 每个号码用各自的 `nums[].pro_url`；EPC 的 OE 号用 `epc.pro_url`（取自 checkNumberData 的 etk 产品，etk/cleanData 不带链接）。
  只有号码是链接、厂商不是，新标签打开，href 只接受 http/https。替换号接口没给链接，保持纯文字。
  **2026-09-14 之前清洗过的数据没有 pro_url，需要用「重新清洗」刷一遍才出链接。**
- 两个单选就是「确认参考产品」（S6a 起落库）：TCD 每个号码一个、EPC 每个产品一个，两列共用一个单选组。

清洗 S6a（2026-09-17，参考 catalog_frey `model/work.php` 的 cleanDataItem / autoCleanCat / cleanWorkChkOkPro，没有照搬）：
- `clean` 一条 = 检测摘要 → **确认参考产品** → **匹配分类** → **深度清洗**。深度清洗失败只返回 `warning`（前 3 步已保存），进度弹窗里标橙色提示，不算失败。
- 自动确认顺序：人工确认过且还在新检测结果里的保留 → TCD 号码清洗匹配的主产品（`tcd.clean.art_id`）→ TCD 第一个分类第一个号码 → EPC 产品。
  人工点单选 = `confirm`（`check_manual=1`），随即重新匹配分类并深度清洗；详情弹窗里可「取消确认」（删深度清洗结果）。
- 分类匹配顺序（`category_from`）：`part` 零件库同厂商同号码 → `tcd_ga` TCD 产品类型查 `part_category_tcd_ga`（只算启用的末级；确认的是 EPC 或未确认时用 TCD 第一个分类）→ `name` 分类中英文名 / 别名完全相同。
  某一步有多个可用末级分类就停：分类留空、`category_candidates` 记候选。`manual` 人工指定的不被自动匹配覆盖；「恢复自动匹配」清掉人工标记后立即重匹配。
  实测数据（2026-09-17）：末级分类上 940 个 ga_id 只有 1 个对应多个分类，16 个 ga_id 只挂在一级分类上（会退到名称匹配）。
- 深度清洗（`WashSheetItemClean`）：TCD 调 `tcd/cleanData`（art_id + 分类清洗率，没分类用 70/50）取号码 / 其它号码 / 国际车型（model + model_oknum 按 typ_id 合并）/ 参数；
  EPC 调 `etk/cleanData` 取参数、OE 号取自检测摘要；两者再用 OE 号（明细本身是 OE 时 + 建议的 OE 号 + EPC 检测的 OE，最多 50 个）调 `etk/getReplaceNumber` 取替换号、`number/getModelByNumber` 取中国车型。
  **按 art_id 调 `tcd/cleanData` 响应能到 900KB**（跟按号码调的 60KB 不是一回事），裁剪后 gzcompress 存 MEDIUMBLOB，实测 26–31KB。
  国际车型 `typ_id` = 国际车型库 `car_vehicle_source_id`，`getModelByNumber` 返回的 ID = 中国车型库 `car_vehicle_source_id`，存结果时直接解析成 `car_vehicle_id`（查不到的留 0，详情里标「车型库里没有」）。
  `art_show=org` 时号码 / 车型都不给建议值（沿用参考系统规则）。重新深度清洗会清空人工勾选。
- 清洗详情弹窗：表头显示确认产品 / 分类（可改）/ 清洗时间与是否人工勾选；五个标签页（号码、替换号、国际车型、中国车型、参数），前四个可勾选，没保存过显示建议值；
  「保存勾选」只收结果里存在的 key；分类清洗率和本次结果不同、确认产品已变（过期）、art_show=org 时顶部提示。
- 表格列：分类（显示名可过滤排序，整格点击改分类）、深度清洗（建议数 / 总数、过期提示、「清洗详情」入口）；
  TCD / EPC / 已确认数据 / 深度清洗 / 零件五列筛选行是「全部 / 有 / 无」（2026-09-28）：列绑定 Model 计算列 `wash_sheet_item_{tcd,epc,check,deep,part}_has`（1/0），单元格仍按原结果字段渲染（`washClean.js` 的 `hasColumn()`），导出也是「有 / 无」；TCD/EPC 有 = `$.matched` 为 true，已确认 = `check_from` 非空，深度清洗 = 已确认且有子表结果（过期也算有），零件 = `lnk_part_item_id` 非空（零件已删除算无）；工具条「匹配分类」「修改分类」对勾选行批量处理（上限 1000）。
- S6b 转零件已拍板：同分类只命中 1 个零件就合并（已审核通过的零件跳过并提示），命中多个跳过交人工，没命中新建草稿；数据读 `WashSheetItemClean::effectiveByItem()`。
- 已知：详情接口一次返回全部车型，刹车片这种约 800KB JSON（本机 0.05s），暂不分页；2026-09-17 之前清洗过的明细没有确认 / 分类，需要「重新清洗」或「匹配分类」。

转零件 S6b（2026-09-17 用户拍板，逻辑在 `model/WashSheetItemPart.php`）：
- 前置：启用的末级分类 + 深度清洗结果 + 没过期，否则跳过说明原因；前端逐条调 `toPart` + 进度条，不限条数；弹窗「跳过已转过的明细」默认勾选
- 号码：明细本身（标 system，新建时为主号码）+ 勾选号码（num/othnum 标 tcd、epc 标 epc）+ 勾选替换号（厂商 OE、标 epc），厂商 + 格式化号码去重；
  OE 号（is_oe）的 brand 是汽车品牌，厂商取 OE（实测 TCD 的 OE 号全是 AUDI 这种品牌名，不这么处理会全部被跳过）；其它厂商名没登记的跳过该号码并提示
- 目标：明细记录的零件还在 → 直接合并；否则同分类按号码匹配，1 个合并 / 多个跳过列候选 / 0 个新建草稿（中英文名取分类名）；目标不是草稿跳过
- 写入一个事务：补号码（不改原主号码）、补车型（国际标 tcd 含适用条件、中国标 yiparts，车型库没有的跳过）、`part_source` 整行覆盖成本次快照、主号码缓存 / touch / 检索、明细记 `_lnk_part_item_id` + `_part_time`
- OE 号的汽车品牌（用户确认用文本）写 `part_number_car_brand_text`：同号多个品牌英文逗号拼接、超过 255 的不再追加，合并时已有号码只补品牌；EPC 的 brand 是 OE 不算品牌；零件详情「号码」分页新增可编辑列「汽车品牌文本」，`part_number_brand` 多选不变
- 参数不写 `part_param_value`（参数定义还没迁移），只存 `part_source_raw_params`

`wash_sheet_item_format` 是号码的格式化值（`formatNum()` 生成：全角转半角 → 只留字母数字 → 转大写），
后台自动维护、界面不显示：单条新增/修改走 `WashSheetItem::applyNumberFormat()`，批量导入在组行时直接算好；
前端提交的该字段一律丢弃（`stripReadOnly()`）。本次没改号码时不动这个字段，避免把已有值刷空。

> 注意：`admin/action/wash.php`（`wash/getModelByNumber` 等调试端点）跟本组模块无关；`model/Wash.php` 是两边共用的 ypAPI 客户端，改方法签名前先查调用点。

## 零件库

| 模块 | Model | Action | View | 状态 | 备注 |
|------|-------|--------|------|------|------|
| 零件库（结构设计） | — | — | — | ✅ 完成 | 方案 v6 定稿 |
| 零件库（建表 + 初始数据迁移） | — | — | — | ✅ 完成 | 2026-09-15 经用户授权在 192.168.1.188 / pdc 执行：`sql/part_library_v1.sql`（45 张新表 + 种子数据）、`sql/migrate_part_library_v1_data.php`（数据迁移）；回滚见 `sql/part_library_v1_rollback.sql` |
| 零件库（Model / Action / View / 菜单） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-16 S1 完成：菜单「零件库（`fa-cubes`）/ 零件」、列表（分类树 + 全文检索 + 号码批量查询）、新增、基本信息 + 多语言、号码、审核、删除；保存流程已接 `PartItemSearch::refresh()`。详见下方会话记录 |
| 零件库（分类管理 + 参数管理 + 参数分页，S2） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-16：新增菜单「零件库 / 分类」（dxTreeList），分类 CRUD + 换父节点（层级/末级标记程序自动维护）+ 其它语言翻译；末级分类编辑弹窗内「参数管理」（挂载/新建全局参数 `part_param`，含 select/multiselect 的选项）；零件详情页新增「参数」分页，按分类挂载动态生成表单。CLI 51 项 + HTTP 全端点自测通过，语言包新增 75 条已译。详见下方会话记录 |
| 零件库（图片文件分页 + zip 批量图片，S3） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-16：零件详情页新增「文件」分页（图片/图纸/附件统一管理，分片上传、主文件互斥、图片可打标签；**2026-09-21 拆成「图片」「文件」两个分页**）；零件列表页新增「批量导入图片」按钮，zip 按「厂商/号码/序号[_标签].扩展名」目录式命名批量导入，全库按号码匹配零件，未匹配/多匹配/零件不可编辑均跳过并在结果里列出。CLI 36 项 + HTTP 端到端（真实建 zip、真实分片上传、真实批处理）自测通过，语言包新增 17 条已译。详见下方会话记录 |
| 零件库（适用车型，S4） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-16：零件详情页新增「适用车型」分页——按车型库层级（国际 3 级/中国 5 级，层级定义读 `car_library_level`，不写死层数）逐级下拉展开到车型叶子层，或按名称关键字搜索；候选车型表格勾选后批量加入，已挂过的跳过不报错。CLI 29 + HTTP 端到端自测通过，语言包新增 22 条已译。**`part_item_brand` 经用户确认不需要**（改为 `PartVehicle::brandsByItem()` 现查现算，分页顶部「适用品牌」实时显示，CLI+HTTP 各追加验证通过，语言包再补 2 条），删表 SQL `sql/migrate_drop_part_item_brand.sql` 待人工审核执行；`part_item_summary` 仍未做，详见下方会话记录 |
| 零件库（关联 / 区域 / 安装位置 / 来源，S5） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-16：零件详情页新增「关联」（link/pair/assembly，只支持选已有零件，两端地位对等的类型按数值大小规范 a/b 防止反向重复）、「区域」（适用国家/区域多选，空=不限）、「安装位置」（按维度分组多选，`part_category_position` 有配置才限定、没配置按不限）、「来源」（只开放确认来源手工设置，清洗快照字段留给 S6）四个分页。新增 Model `PartRelation` / `PartItemRegion` / `PartItemPosition` / `PartSource` / `SysRegion`。CLI 33 项 + HTTP 端到端自测通过，语言包新增 44 条已译。`part_category_position` 管理界面已在同日追加（见下方会话记录），不再是「留给以后」。详见下方会话记录 |
| 零件库（UI 优化第一批：分页合并 / 统一保存 / 批量操作，S7a） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-19：详情页 9 个分页并成 6 个（区域 / 安装位置 / 来源并进基本信息），5 个分散的保存按钮收成页头一个（Ctrl+S、分页标题打未保存圆点）；文件类型按扩展名自动判定、适用车型改懒加载层级树 + 输入即搜、关联零件改远程搜索下拉、号码失焦即查重、提交 / 通过不再多弹一层窗；列表页加多选 + `partItem/batchAudit` / `batchUpdate` 批量审核与批量改基本信息。无 DDL。HTTP 33 + 文件类型端到端 12 项 + 浏览器实测通过，测试数据已清；语言包新增 25 条已译（删废弃 13 条）。**分类页（partCategory）本次没动**，见下方会话记录里的「待办」 |
| 零件库（安装位置语义修正） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-19：零件能选的安装位置**完全**由分类的 `part_category_position` 决定——**分类没配置 = 该分类的零件没有位置可选**（原先是「空 = 不限，可选整个字典」）。改 `PartItemPosition`（注释 + `replaceForItem` 校验）、`PartCategoryPosition` 注释、零件详情候选过滤、分类页提示文案；DB.md / API.md 已改口径（**注意与「无记录=不限」的常见惯例相反**）。现有 `part_item_position` 1 行在新规则下仍合法，无需迁移。HTTP 15 项 + 浏览器实测通过；语言包新增 2 条已译 |
| 零件分类选择器（全项目统一） | — | — | ✅ | ✅ 完成 | 2026-09-19：分类末级有几百个，下拉翻不动——新增 `static/js/lib/partCategoryPicker.js` + `css/partCategoryPicker.css` + `view/public/partCategoryPickerText.view.php`（文案 + css/js 一起引），`createPartCategoryPicker($host, opts)`：只读输入框显示完整路径，点开弹窗两个分页（「选择分类」按层级铺开打钩、「查询分类」关键词找，结果带路径）。**全项目 5 处分类选择已全部换掉**：零件新增弹窗、零件详情基本信息、零件列表批量修改、数据单明细「修改分类」、中国车型统计的分类多选（`multiple` + `leafOnly:false`）；旧的 `partCategorySelectOptions()` 和统计页的树形下拉已删。无 DDL、无接口改动。5 处全部浏览器实测通过；语言包 686 条（新增 12，en 已译、ru 占位）。**dxForm 里它是模板项**：`validationRules` / `readOnly` 管不到，必填、禁用、写回 formData 都要自己做（写进 UI.md） |
| 零件库（适用区域改由车型推算） | ✅ | ✅ | ✅ | ⚠️ 有问题待处理 | 2026-09-19：用户拍板**零件的适用区域由零件对应车型的适用区域推算，零件本身不设置**（口径同 `part_item_brand`）。已删 `model/PartItemRegion.php` / `admin/action/partItemRegion.php` / 详情页的区域勾选树 / `partItem/detail` 注入的 `regions`；删表 SQL `sql/migrate_drop_part_item_region.sql`（v1.0.7，表 0 行；v1.0.6 已被 `migrate_drop_remember_token_fk.sql` 占用）**待人工审核执行**。`sys_region` 等三张区域字典表和 `SysRegion` 保留。**未完成**：车型侧目前没有任何区域数据（`car_vehicle` / `car_node` / `car_library` 都没有 region 列，`extra` 里也没有，没有 `car_*_region` 映射表），推算链路断在最后一环，`regionsByItem()` 和界面展示位等数据来源确定后再做 |
| numberVendor（号码厂商） | ✅ | ✅ | ✅ | ✅ 完成 | 表 `attr_factory`，字段 `attr_factory_id` / `attr_factory_name`（2026-09-14 迁移已执行）；被明细使用的厂商不能删。2026-09-17 菜单从「零件关联属性」移入「零件库」，原菜单组已删 |
| carBrand（汽车品牌） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-17：`car_brand` 增删改 + 展开行维护其它语言译文（新 Model `CarBrandLang`）；被引用的不能删。2026-09-20 按用户要求精简列表：去掉「排序」「映射车型节点数」「号码引用数」「匹配关键字」四列（两个统计的子查询也从 Model 拿掉），`car_brand_sort` 保留为隐藏排序列（只为让请求带上默认排序），新增时自动取 max+1 排到最后；`car_brand_keyword` 改为后端自动生成（`CarBrand::refreshKeyword()` = 中文名 + 英文名 + `car_brand_lang` 译名），界面既不填也不显示，存量 133 行已重算。无 DDL |
| carSeriesIntl（国际车系） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-19：对应参考系统 `carmodel/arealist`。菜单「零件库 → 车型 → 国际车系」（排在国际车型上面）；新 Model `CarSeries`（`car_node` 最深一层非车型节点，层级不写死）+ Action `carSeriesIntl`（GridActions + `parents`）+ 视图 `carSeries/index`（左车厂列表带车系数 / 车型数 / 英文名，右车系表格，弹窗增删改）+ `css/carSeries.css`。无 DDL。CLI 21 项通过、测试数据已清；语言包新增 17 条已译（703）。浏览器实测通过（车厂过滤、点选限定、新增自动来源ID、删除、有车型拦截、左侧计数刷新），测试数据已清 |
| carVehicleIntl（国际车型） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-17：`CarVehicle::scopeToLibrary()` 按层级联出车厂 / 车系；弹窗逐级下拉选上级；来源ID（TCD typ_id）同库唯一；被零件适用车型引用的不能删。视图与中国车型共用 `carVehicle/library` |
| carVehicleCn（中国车型） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-17：只读列表（品牌 / 生产商 / 车系 / 车代 + 车型字段 + 扩展属性），`save` / `remove` 服务端拒绝 |
| 中国车型「同步车型」 | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-19：新 Model `CarVehicleSync` + `carVehicleCn/syncRun` / `syncProgress` / `syncReport`，中国车型列表工具条加「同步车型」按钮（「先预览」只比对不写库 /「开始同步」真写），弹窗内有真实百分比进度条（服务端写进度文件、前端每秒轮询，`syncRun` 先 `session_write_close()` 免得锁住轮询）。从宜配 `client/getYpcModel` 拉全量（8.3 万行 / 64MB），按祖先链还原成 `car_node` 四层再比对；被零件引用的车型和还挂着东西的节点一律不删只标「保留」；接口没有的列保持原值。**两道安全闸**：响应必须是车型列表（接口偶尔返回 4 键状态包）、行数不得低于现有的 90%。配置在 `config/common.php` 的 `ypModelAPI`。CLI 38 + 安全闸 12 项 + HTTP + 浏览器实测通过，语言包新增 45 条（674）。**库已同步到位**：car_vehicle 80127→83367、car_node 9113→9370、驱动方式 0→83028 行有值，完整性校验全过 |
| 车型菜单三级化 + carVehicleCn/report（中国车型统计） | ✅ | ✅ | ✅ | ⚠️ 有问题待处理 | 2026-09-19：`config/admin.php` 把「汽车品牌 / 国际车型 / 中国车型」收进「零件库 → 车型（`fa-car`）」三级菜单（`adminLayout.js` 本来就支持任意层嵌套，前端没改）；新增「中国车型统计」= 品牌 × 零件分类的覆盖率报表（`CarVehicle::brandCoverageReport()` + `carVehicleCn/report` / `reportData` / `reportExport`，新视图 `view/carVehicle/report.view.php` + `css/carReport.css`）。品牌层取车型库最浅一层非车型层级，不写死层数；选大类含其下级，覆盖数按 `COUNT(DISTINCT car_vehicle_id)`。无 DDL。浏览器实测通过（本田 959 款车型 / 刹车片覆盖 2 款 = 0.21%），xlsx 导出实测 13KB 有效文件；语言包新增 9 条（629）。「所属车型品牌」经用户拍板**就是本菜单的「汽车品牌」**，不新建字典、不加菜单（参考库自己已把 `cd_make` 并进 `tag_brand`，`car_brand` 就是从 `tag_brand` 迁来的），**无 DDL**；顺带补的俄文译文 SQL `sql/migrate_car_brand_lang_ru.sql`（v1.0.8，18 行 DML）待人工执行 |
| 零件库（审核表结构） | — | — | — | ✅ 完成 | 2026-09-15 经用户授权执行 `sql/migrate_part_item_audit.sql`（v1.0.2）：`part_item_audit_status` 注释改为 0/1/2(/3 预留)、新建 `part_item_audit_log`；回滚见 `sql/migrate_part_item_audit_rollback.sql` |
| 零件库（审核改两状态：待审 / 已审） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-24：取消草稿，编辑 / 删除不受审核状态限制、编辑不改状态、可自审；`assertEditable()` → `assertExists()`，`insertDraft()` → `insertItem()`（新建为待审）；数据迁移 `sql/migrate_part_item_audit_two_status.sql`（v1.0.9：0→1、删 submit/withdraw 审核记录、默认值改 1）2026-09-24 经用户授权已执行（sys_migration id=11），现 待审 353 / 已审 149 |
| 框架配合：Grid 计算列筛选排序（多语言显示名） | — | — | — | ✅ 完成 | 2026-09-15：`Model::gridComputedColumns()` / `gridComputedSelect()` + `Lang::translation()`，CLI 自测 46 项通过；`attr_type` / `attr_value` 排序规则已统一为 `utf8mb4_unicode_ci`（`sql/migrate_attr_collation_unicode_ci.sql` 2026-09-15 经用户授权执行，预览全 0、行数 6 / 58 不变、回退表达式不再报 1267） |
| 框架配合：文件上传（`sys_file`） | ✅ | ✅ | — | ✅ 完成 | 2026-09-15：框架 `lib/UploadedFile` / `FileUploader` / `FileStorage` / `LocalFileStorage` / `UploadException`；业务 `model/SysFile`、`file/upload` / `view` / `download` / `options`，配置 `config/upload.php` + `common.php` 的 `uploadDir`。只实现 local，oss 留接口。CLI 36 项 + HTTP 自测通过，测试数据已清。**待人工**：php.ini 本机 `upload_max_filesize=2M`（配置 50MB，按小的生效）；`pdc/storage/upload` 设 `svn:ignore`；生产 Nginx deny 存储目录或配到 Web 根外；零件界面未接 |
| 框架配合：分片上传 + zip 安全解压（S0） | ✅ | ✅ | — | ✅ 完成 | 2026-09-16：框架 `lib/ChunkUpload` / `ZipSafeReader` / `ZipTask` / `UploadTempDir`，`FileUploader::maxSize()` 改为只看配置（新增 `directMaxSize()`、`inspect()` 可传上限）；业务 `file/chunk` / `chunkStatus` / `zipChunk` / `taskNext` / `taskDiscard`、`static/js/lib/chunkUpload.js`；配置 `common.php` 的 `uploadTempDir` / `zipMaxSize`(2GB) / `zipMaxFiles`(5000) / `zipMaxUnpackedSize`(4GB) / `zipMaxRatio`(100)、`upload.php` 的 `chunkSize`(1MB) / `tempExpire`(24h)。CLI 88 项 + HTTP 43 项自测通过，测试数据与文件已清。**待人工**：`pdc/storage/temp` 设 `svn:ignore`；生产关 `display_startup_errors`；零件图片批量导入（S3）未接 |
| 框架配合：语言包按启用语言生成（`sys_language`） | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-15：`Mvc::init` 的 `locales`（业务层传 `SysLanguage::enabledNames()`）→ `?lang=` 只收启用语言、切换菜单动态输出；`LangScanner` 按启用语言同步，停用语言文件保留不动；DevExtreme 无消息包回退 en。CLI + HTTP + 浏览器自测通过，测试语言 `ko` 行与 `ko.php` 已删。未做：登录后台后的 langTool 页面实测 |
| 框架配合：全文检索（`part_item_search`） | ✅ | — | — | ✅ 完成 | 2026-09-15：框架 `lib/FullText`（BOOLEAN MODE 条件 / 相关度，短词 LIKE 回退）+ `Model::gridWhere()` / `gridDefaultOrder()`；业务 `model/PartItemSearch`（refresh / refreshMany / refreshByCategory / refreshByFactory / refreshAll + condition / relevance / searchIds）。CLI 自测 60 项通过、测试数据已清。`sql/part_item_search_ngram_stopword.sql` 2026-09-15 经用户授权执行（索引已关停用词重建，表 0 行，`pad` / `Air` 效果待有数据后验证）；零件保存流程未接刷新 |

迁移结果（2026-09-15 核对目标库）：字典 40 条（unit 15 / data_source 13 带编码 / develop_status 4 / pair_type 4 / image_tag 4）+ 安装位置 5 维度 13 取值；
`attr_factory` 1,454（原 OE、TRW 的 ID 不变，OE 类型已标）；`sys_region` 区域 32 + 国家 252、关联 621；`car_brand` 133；
`part_category` 21 + 385；`part_category_tcd_ga` 1,949（256 条无名称）；`part_category_aaia` 84（3 条无名称）；
`car_node` 国际 21,911 / 中国 9,113（生产商节点 443，因 36 个生产商横跨多个品牌）；`car_vehicle` 国际 165,230 / 中国 80,127；`car_brand_node` 430。
待人工处理的核查项：国际车厂未映射品牌 917、中国品牌未映射 181、节点映射多个品牌 43、源库孤儿映射 18 条已跳过；
中国车型功率非单个数字 703（原文保留，数值取第一个数）、1 条功率脏数据 `221220257199`（mod3_id 349390，数值置空）、8 条结束年早于开始年（照原样导入）。
表结构相对方案页的补充：`sys_region_source_id`、`car_brand_source_id`（追溯来源）、`car_node_parent_key`（生成列，唯一键兜底）。

参考系统：`http://192.168.1.44/catalog_frey/pdc/admin/`「零件库 → 零件」，库 `192.168.1.42 / frey_catalog`（只读学习）。
参考库核心表：`oe_item` 零件、`oe_numidx` 号码、`pro_factory` 号码厂商、`pro_cate` 分类、`part_param` / `part_param_sche` / `oe_param_val` 参数、
`m_make` / `m_series` / `m_model` 国际车型、`m_cn_model` 中国车型（平铺）、`oe_model_link` / `oe_model_cn_link` 车型关联、
`tag_brand*` 汽车品牌、`oe_pic` + `files` 图片、`oe_link` 零件关联、`crm_country*` 适用国家、`object` 字典。

方案 v3（2026-09-14，完整字段定义见 Artifact「零件库一期数据结构」https://claude.ai/code/artifact/f0bd508c-3e21-4451-bd2e-7242605e34e5 ）：
- 系统/字典：`sys_language`（字符串主键）、`sys_migration`、`sys_file`；`attr_factory` 加 `attr_factory_type`；`attr_type_lang`、`attr_value_lang`；新字典类型 unit / data_source / pair_type / part_position（树）/ develop_status / image_tag
- 分类参数：`part_category`(+`_lang`)、`part_category_tcd_ga`（存 TCD 产品类型 ID + 名称缓存）、`part_category_aaia`、`part_category_competitor`、`part_category_position`、`part_param`(+`_lang`)、`part_param_tcd_name`、`part_param_option`(+`_lang`)、`part_category_param`
- 零件：`part_item`(+`_lang`)、`part_number`、`part_number_brand`、`part_param_value`、`part_item_position`、`part_source`、`part_file`、`part_file_tag`、`part_relation`
  （`part_item_region` 2026-09-19 已确定不需要，删表 SQL 待执行）
- 车型：`car_library`(+`_lang`)、`car_library_level`(+`_lang`，层级由提供商定义，名称是数据不走 `lg()`)、`car_node`、`car_vehicle`、`car_brand`(+`_lang`)、`car_brand_node`、`car_sync_log`、`part_vehicle`（所有车型库共用）
- 区域：`sys_region`(+`_lang`)、`sys_region_country`
- 派生：`part_item_summary`（按零件+车型库）、`part_item_brand`、`part_item_search`（ngram 全文）

已确定：
- 产品化：一客户一库，不带租户字段；车型库层级由软件提供商定义，客户只能启用/停用
- 一期范围：全部功能纳入（含适用国家、开发状态、产品经理/采购、多语言）
- 多语言：中英为主表固定列，其它语言每个实体一张 `_lang` 表；中文必填、英文选填；回退 当前语言 → 英文 → 中文；`sys_language` 标记内置（中英）/非内置；翻译实体 10 个（零件、分类、参数、参数选项、属性类型、属性值、汽车品牌、区域、车型库、车型库层级）；`lg()` 只用于界面文案，库里的数据一律不走语言包
- 命名：主键 `{完整表名}_id`；字段 `{完整表名}_xxx`；外键 `{本表}_lnk_{被引用完整主键}`；同表多外键加语义后缀（如 `_operator` / `_auditor`），指向 `attr_value` 的外键一律带字典类型后缀；只有两端对等的关系才用 `_a` / `_b`（已写入 DB.md，2026-09-15）
- 车型：初始迁移**只迁车型本身**（frey `m_*`，国际 165,230、中国 80,127），不迁零件↔车型映射（2026-09-15，用户更正前一轮的手误）。导入规则：来源 ID 用宜配 ID；国际层级 车厂→车系→车型，中国层级 品牌→生产商→车系→车代→车型；`car_node` 唯一键为（上级, 层级, 来源 ID），因为中国车型有 36 个生产商横跨多个品牌；功率存原文 + 解析值（中国车型 703 条非纯数字）
- 初始数据（只迁中文、英文，小语种翻译不保留；零件、号码不迁）：
  - 字典：frey `object` 的计量单位 15、数据来源 13、开发状态 4、配对类型 4、图片标签 4；另建安装位置字典；当前系统字典的实验数据用户已清空
  - 基础数据：frey `pro_factory` → `attr_factory`（1,456）、`tag_brand` → `car_brand`（133）；品牌↔车型节点映射 `tag_brand_make` / `tag_brand_cnbrand` → `car_brand_node` 照原样迁（只覆盖约 77% 车型、含错误多重映射），迁完导出未映射 / 多重映射清单人工补正、`crm_country` → `sys_region`（32 区域 + 252 国家）
  - 分类：**ypdb** `yp_part` + `yp_part_tree` → `part_category`（两级 21 + 385，名称取 `yp_lng_des`→`yp_lng_text`，中文 lngid=1 编码 `cn`、英文 lngid=2）；`yp_part_tcdga` → `part_category_tcd_ga`（1,949 条，名称从 `tecdoc_formal_2026v2.tof_generic_articles` 补，256 条补不到）；`yp_part_aaia` → `part_category_aaia`（84 条、77 个分类，英文名从 `ypdb_aaia.yp_part` 补，3 条补不到）；frey `pro_cate` 不再迁移
  - 参数：暂不迁移（ypdb `yp_para_label` 1,938、`yp_para_part_label_link` 1,893，以后需要时再迁）
- 图片标签：一期要，`part_file_tag`（文件 ↔ image_tag 字典，多选）
- 安装位置：同维度允许多选
- 「借用」标记、SYS 类号码（集团中性号/物料编码）：暂不处理

零件界面已拍板（2026-09-15）：
- 菜单「零件库（fa-cubes）/ 零件」；列表左分类树 + 右 Grid + 全文检索框，默认按更新时间倒序；新增走小弹窗（分类 + 主号码厂商 + 主号码 + 中文名）后打开详情标签 `partItem/detail/k/itemId/{id}`，详情内分页标签各自保存
- 后端一个子表一个 Model；派生数据（主号码 / 主图缓存、检索内容、车型汇总）由 Model 维护
- 同一号码允许挂在多个零件上，保存时提示「已存在于零件 X」，不拦截
- 删除为物理删除（子表级联），任何审核状态都能删（2026-09-24 改）
- 审核流程（2026-09-24 用户改口径）：只有 1 待审 / 2 已审两个状态，取消草稿；动作只有审核、反审核（回到待审）；**零件数据的编辑是永久性的，不受审核状态限制**，编辑已审零件也不改变审核状态；审核记录表 `part_item_audit_log`
- 自审：2026-09-24 用户拍板可以自审，`partItem.allowSelfAudit` 开关已删除
- 「零件」分会话：S0 框架层分片上传 + zip 安全解压；S1 菜单 / 列表 / 新增 / 基本信息 + 多语言 / 号码 / 审核 / 删除；S2 分类与参数管理 + 参数分页；S3 图片文件分页 + zip 批量图片；S4 适用车型；S5 关联 / 区域 / 安装位置 / 来源；S6 数据单导入零件
- 上传不依赖 php.ini：前端 dxFileUploader 分片（每片 1MB）；zip 上传后服务端安全解压，前端驱动分批处理并出结果明细；zip 默认上限 2GB，在 `pdc/config/common.php` 配置（单包文件数建议默认 5000，同处配置）
- zip 批量图片命名用目录式：`号码厂商/号码/序号[_标签].扩展名`，号码按 `formatNum()` 匹配，序号 1 为主图，标签按 image_tag 字典中文名或编码匹配；同一文件已挂在该零件上时跳过；一个号码匹配到多个零件时**跳过并在结果里列出**；零件已有图片时**追加**

拍板事项已全部落地（S1、S2、S3、S4、S5 见下方会话记录）。S6 拆成 S6a 清洗（2026-09-17 已完成，见「数据清单」）和 S6b 转零件（2026-09-17 已完成，见「数据清单」）。
需框架配合（单独会话）：文件上传、全文检索查询封装（2026-09-15 已完成，见上表）（Grid 计算列筛选排序已于 2026-09-15 完成，用法见 DB.md「Model 使用规则」；语言包按启用语言生成已于 2026-09-15 完成，规则见 `contracts/i18n.md`）

## 商品库

| 模块 | Model | Action | View | 状态 | 备注 |
|------|-------|--------|------|------|------|
| 商品库（结构设计） | — | — | — | ✅ 完成 | 2026-09-28：只读学习参考站 catalog_frey「商品库」菜单 + 参考库；方案页 https://claude.ai/artifact/MpNEXbnV1ZbvUmSJP4KFNN ；拍板规则见 DB.md「商品库规则」 |
| 商品库（P0 建表 + 字典） | — | — | — | ✅ 完成 | 2026-09-28 经用户授权在 192.168.1.188 / pdc 执行 `sql/pro_library_v1.sql`（v1.1.0，sys_migration id=12）：30 张表、75 个外键；通用属性新增 item_type 4 / material 23 / pro_status 9 / pro_unit_type 1。回滚 `sql/pro_library_v1_rollback.sql`。约束冒烟 5 项通过（事务回滚，无残留） |
| P1 商品品牌 / 经营单元 / 分类商品属性 / 数据权限 | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-28：菜单「商品库 → 商品品牌 / 经营单元」（`proBrand` / `proUnit` + 翻译 `proBrandLang` / `proUnitLang`）；分类详情页（末级）新分页「商品属性」（`proCategoryExt/info` / `save`）；角色页加「可见商品品牌 / 可见经营单元」，解析器 `ProDataScope`（P3 / P4 列表用）。无 DDL。品牌编码规则列留给 P2；经营单元菜单名可配置未做 |
| P2 编码规则引擎 | ✅ | ✅ | ✅ | ✅ 完成 | 2026-09-29：菜单「商品库 → 编码规则」（`proCodeRule` 规则与段 / 码表两页签 + 预览）；引擎 `ProCodeEngine`（preview / previewFamily / generate / match）；对齐参考站 gsp：规则适用条件（品牌 + 分类 + 编码扩展）+ 手动、表达式双向、区间流水、截取上级编码、编码套、通用属性码表；插件骨架 `config/product.php` + `plugin/frey/`。SQL `pro_code_rule_v2.sql` v1.1.2 已执行；品牌 中性 MAIN（主）/ 华配 HUA / 夏配 XIA 已建。**待办**：langTool 扫描（新文案未译）；码表 / 老编码计数器接续留给迁移会话 |
| P3 零件生成主商品 + 商品列表 / 详情 + 检索表 | ❌ | ❌ | ❌ | ❌ 未开始 | **同时要改 `PartItem` 删除：被商品引用时先查再友好提示**（现在只会撞外键 1451） |
| P4 品牌商品（汽车品牌范围、按品牌页签） | ❌ | ❌ | ❌ | ❌ 未开始 | |
| P5 经营单元商品 + 扩展字段（筛选排序） | ❌ | ❌ | ❌ | ❌ 未开始 | 按 33 万行 × 40 字段规模实测性能 |
| P6 零件变更传播 / 变更日志与同步队列骨架 / 导出 | ❌ | ❌ | ❌ | ❌ 未开始 | |

## 框架层待处理

| 问题 | 文件 | 状态 |
|------|------|------|
| ~~PHP 版本兼容性（构造函数提升/联合类型）~~ | `mvc/v2/Model.php` | ✅ 作废 |
| 全项目统一表/字段命名（`admin_user`、`attr_factory` 及 17 个外键列改名，Session 键同步改，清空 `sys_dxdatagrid` 布局，`mvc/v2/NOTES.md` 有效内容并入 `AGENTS.md` 后删除） | `mvc/v2/lib/Auth.php` 等 + `pdc` 全量 | ✅ 库已迁移（第 0~7 步），冒烟测试 + 浏览器验证通过（本机 127.0.0.1 已是新代码）；其它环境部署时须先迁移库。后续：框架两份契约一致性、`settings.local.json` 旧字段名 |

> 上面这条基于"生产 PHP < 8.0"的错误前提，已作废。生产环境确认为 PHP 8+，
> 项目按 PHP 8.1 编写，口径见根契约的「PHP 版本约束」。

## 最近会话记录
- 2026-09-28 / 29：商品库 P2 编码规则引擎——规则 / 段 / 码表维护页、引擎（取号旁路补行 + FOR UPDATE，复用登记不加锁读，回滚不跳号）、预览不占号、插件骨架；商品品牌页加「编码规则」列（品牌默认规则）。用户拍板：码表编码允许重复标黄、样例规则 FREY_MAIN / FREY_BRAND（`pro_code_rule_sample_frey.sql` v1.1.1）、被引用规则不让删可停用、老编码接续留迁移；9-29 按参考站 gsp 编码规则补齐：内部编码不做、区域不做、编码扩展进通用属性（树形 `pro_code_ext`）、按品牌 + 分类自动选规则、表达式双向，经授权执行 `pro_code_rule_v2.sql`（v1.1.2），建品牌 中性 MAIN（主）/ 华配 HUA / 夏配 XIA。CLI 57 + 68 + 并发 13 + 表达式 35 + HTTP 20 项通过，浏览器（伪造超管会话的 php -S）实测规则表单 / 段弹窗 / 预览 / 码表，测试数据已清；DB / API / UI / i18n 契约已同步；**langTool 未扫描**
- 2026-09-28：商品库 P1——商品品牌（标识码 / 中文名唯一、主品牌最多一个先查再报、品牌有商品后不能改主品牌标记、号码厂商范围随行全量替换）、经营单元（类型取 pro_unit_type，只有一个值时默认带上）、两者翻译；分类详情末级新分页「商品属性」接入页头保存；角色页数据权限两列 + `ProDataScope`。用户拍板 3 条：多角色只看设置了范围的角色取并集（都没设 = 不限制）、商品属性只对末级分类、被范围引用的品牌 / 单元 / 号码厂商拦下不让删。`dxMultiSelectColumn` 加可选 tagBox 配置（厂商 1454 个要能搜）。CLI 53 项 + HTTP 冒烟 + 浏览器（伪造超管会话的 php -S）实测通过，测试数据已清；语言包 +53（en 已译，ru 与全包一样是占位）
- 2026-09-28：商品库 P0——用户拍板 7 项（前缀 `pro_`、必须从零件生成、零件被引用不能删、编码不改、无品牌 OE 默认显示、生成即审核 + 8 主状态、4 字典进通用属性），经授权执行 `sql/pro_library_v1.sql`（v1.1.0）：30 表 + 字典 37 值；DB.md 加 `pro_` 前缀（`biz_` 作废）和「商品库规则」
- 2026-09-28：商品库（零件库转商品库）只读学习参考站 catalog_frey「商品库」菜单（商品 / 小类 / 编码规则 / 商品品牌 / 号码厂商 / 品牌商品 / 事业部商品）+ 参考库表结构，未写代码；出「配置 + 插件」的多客户商品库方案草案，待用户拍板（见当次会话回复）。已拍板：一期三层全做；商品分类直接复用零件分类；扩展字段必须可筛选、可排序；车型 / 号码 / 参数大部分共用、各商品品牌有差异（如保时捷品牌只显示保时捷车型）→ 方案走「继承 + 覆盖」（用户：暂时符合，后续再看）；外部同步一期只做骨架；「事业部」泛化为「经营单元」。数据结构草案 v1（`product_` 前缀约 30 张表 + 分会话计划 P0–P6）见 Artifact「商品库一期数据结构」https://claude.ai/artifact/MpNEXbnV1ZbvUmSJP4KFNN ，文末 7 条待确认，SQL 未写
- 2026-09-28：零件列表「重置」改成整体重置（用户定）——`dxGrid.js` 新增 `toolbar.onReset(dataGrid)` 钩子（重置时先调、之后照常清过滤 / 排序 / 分组 / 已选并刷新一次）；零件列表用它清号码批量查询和分类筛选（新 `clearCategoryFilter()`，「全部分类」也改用它）；删掉顶部右侧「按号码查询: n 清除」提示（`#partNumberFilter` + 样式 + 词条「按号码查询」zh-CN / en / ru 同步删），号码条件只在弹窗里看和清。临时页（`$.ajax` 打桩记请求）验证：查 2 个号码后重置 → numbers 为空；点分类树后重置 → categoryId 0、「全部分类」高亮、树无选中；后台登录态实测未做。UI.md 记了重置口径
- 2026-09-28：零件详情「基本信息」加只读「清洗来源」区块（备注下面、安装位置上面）——来源标签（TCD / EPC / 自行确认）+ 来源号码「厂商: 号码」（链上游产品页，只放行 http(s)）+ 产品名（当前语言在前、另一语言灰字）+ 中英文参数两列（名称 / 值，样式同清洗详情产品卡片）；没有快照显示「暂无清洗来源」。数据：新 `PartSource::displayByItem()` 解 `part_source` 的 `check_info` / `raw_params`，`partItem::detail()` 以 `washSource` 带进页面，不另发请求（重新转零件后刷新页面才更新）。无 DDL、不改保存逻辑；语言包 +4（清洗来源 / 暂无清洗来源 / 来源号码 / 自行确认，en 已译、ru 占位），待 langTool 扫描。验证：CLI 零件 63 输出 tcd / SWAG: 33 11 0845 / 2 条参数；CLI 渲染的临时详情页 63 显示正确、无横向滚动，零件 555 显示「暂无清洗来源」；后台登录态实测未做。API.md 的来源说明同步更新
- 2026-09-28：数据单明细「零件」列和清洗详情页头的零件链接，显示和打开的标签标题改成零件库口径「厂商: 主号码」（原来是 `#ID 主号码`）——`WashSheetItem::gridFrom()` 多关联 `attr_factory wpaf`（零件主号码的厂商），`gridSelect()` 出 `part_item_main_number_display`（拼法同 `PartItem`，无主号码时为空、前端回退 `#ID`），原 `part_item_main_number` 不再取；`washClean.js` 新增 `partItemLink()`，两处共用并把标题传给 `openPartItemDetail()`，和零件库列表点开同一零件的标签同名。无 DDL、无语言包。CLI 实测明细 1：明细 grid / `detail()` / 零件库 grid 都是 `OE: 5Q0 199 868J`，临时页点链接传出 (63, "OE: 5Q0 199 868J")。**未改**：转零件进度弹窗里的零件链接仍是 `#ID`（`/toPart` 返回里没有主号码）
- 2026-09-28：清洗详情页操作调整（用户定）——「重新深度清洗」加确认框（会覆盖结果、已保存的人工勾选恢复为建议值），点「否」不发请求；页头去掉「取消确认」，要换确认产品回列表 TCD / EPC 列重新选。后端 `confirm` 的取消分支（from 为空）保留未动，只是详情页不再调用。文案：删 `cancelConfirm` / `cancelConfirmAsk` 两条（zh-CN / en / ru 同步删），新增 1 条（en 已译、ru 占位），`scan.php` 待 langTool 扫描同步。临时静态页验证：页头按钮只剩 修改分类 / 转零件 / 重新深度清洗，弹框文案正确，点「是」才发 `/deepClean`
- 2026-09-28：清洗详情页（`washSheetItem/detail`）UI 美化——整页浅灰底 + 三张白卡片（页头 / 产品 / 标签页）；页头号码做大标题（厂商小徽标），字段两列网格（确认产品独占一行），字段值改 flex 按基线对齐，操作入口改小号描边按钮（`actionLink()`），清洗率改成两个标签，过期 / 清洗率变动提示合成一条橙边提示条；产品卡片图片加底框，参数由圆点列表改「名称 / 值」两列斑马行；「比较 / 恢复建议 / 保存勾选」并进标签页卡片，放在标签栏上方靠左（同日按用户截图从页脚挪上来）。只改 `washClean.js`（`renderHeader()` / `renderProduct()` 参数行 / `field()`）和 `washSheet.css`，接口、文案不变，无语言包变动。用真实结果（09S409061B）经 CLI 渲染的临时静态页在 1600 / 1300 / 1100 宽验过（无横向滚动、两列对齐），后台登录态实测未做；UI.md 加详情页灰底 / 提示条色值
- 2026-09-28：数据单明细表格美化——两个明细页容器加 `.wash-item-grid`，数据行一律顶对齐（压过主题对勾选框 / 操作列的居中），行高统一 1.6，勾选框下移对齐首行；TCD 分类名悬停只给文字加下划线（放大镜不带）；列内图标 24px → 16px；结果列统一靠左（零件列原先按数字右对齐）。用临时静态页 + 假数据量过各列首行中线，误差 1px 内
- 2026-09-28：数据单明细 TCD / EPC / 已确认数据 / 深度清洗 / 零件五列加「全部 / 有 / 无」筛选（Model 5 个计算列 + `hasColumn()`），无 DDL；SQL 口径对单 #1 实测与界面一致，浏览器实测未做（需登录）；语言包新增「有」「无」2 条待扫描翻译

> 完整版在 [modules.archive.md](modules.archive.md)，按日期查。下面只是一行摘要。

- 2026-09-28：数据单列表加明细统计四列（对应参考系统 catalog_frey `work/dataList`）：明细数、已清洗（有检测时间）、有清洗数据（TCD 或 EPC 任一 matched）、零件库存在（`part_number` 同厂商同格式化号码，明细已定分类时零件分类须一致）。`WashSheet::grid()` 按当前页单号两条 GROUP BY 现算，后三列画比例条（100% 绿），不能筛选排序、不进编辑表单。无 DDL；HTTP + 浏览器实测（单 1：7 / 7 / 6 / 1，单 5：5 / 5 / 3 / 1，与直接 SQL 一致）；语言包 +4 已译 en / ru；UI.md 加比例条色值
- 2026-09-24：零件详情「图片」分页的文件列只显示缩略图、不再显示文件名（缩略图可点开原图，悬停提示文件名）；「文件」分页照旧显示图标 + 文件名。`renderFileLikeTab()` 加 `showFileName` 配置，只改 `detail.view.php`；浏览器实测 #465 图片列只剩缩略图链接
- 2026-09-24：零件详情「图片」「文件」分页去掉顶部的上传区（上传控件 + 说明），改为表格工具条上的「上传图片 / 上传文件」按钮（排在刷新、重置之后），点了开弹窗：说明 + dxFileUploader（多选、逐个文件显示进度）；弹窗关掉只隐藏，没传完的继续传，再开能看到进度，没在传时再开清空上次列表。`dxGrid.js` 的 `toolbar.custom` 新增 `afterBuiltins` 选项控制排在内置按钮前 / 后（默认不变）。只改前端，无文案变化；浏览器实测按钮位置、弹窗开关复用、表格紧贴分页标签、控制台无错误，**未实传文件**
- 2026-09-24：零件审核改为「待审 / 已审」两个状态，取消草稿；零件编辑、删除不受审核状态限制（编辑不改状态），可自审（删 `allowSelfAudit`）；详情页去掉整套只读机制，按钮为 保存 / 审核或反审核 / 删除 / 审核记录；数据单转零件、zip 批量图片不再因状态跳过。SQL v1.0.9 经授权已执行；语言包 +5 条（已译 en / ru）
- 2026-09-23：`createDxGrid()` 默认分页器改为紧凑模式（每页条数下拉 10/20/30/40/50/100/200/300/500/1000 + 页码信息 + 输入跳页 + 上下页），所有主列表页生效；弹窗里的独立表格（零件详情车型选择、清洗结果）不经过 createDxGrid，未改。浏览器实测切 50 条 / 跳第 3 页 / 下一页正常，页面仍无滚动条。UI.md 加「分页器」
- 2026-09-23：修复列表页出现页面滚动条（零件列表）。`dxAutoHeight()` 没扣父元素底部内边距（`#partListPage` 的 8px）；且 DevExtreme 只在初始化时算一次高度，零件页的顶部按钮条在建表格之后才渲染，把表格往下推了约 38px。`dxGrid.js` 的 `dxAutoHeight()` 改为扣掉各层父元素的下内边距 / 边框 / 外边距，`createDxGrid()` 在下一轮事件循环、每次 contentReady、窗口 resize 时重算高度（页面自己传 height 的不管）。浏览器实测 800 / 900 高度下表格底部正好到 792 / 892，无滚动条；部门页不受影响。UI.md 加「表格高度铺满可视区」
- 2026-09-23：按用户要求给模拟零件补图：20 个末级分类各挑 1 个草稿零件（ID 68 / 90 / 117 / 140 / 164 / 189 / 214 / 240 / 265 / 289 / 314 / 341 / 364 / 390 / 414 / 439 / 465 / 489 / 515 / 545），走真实接口 file/upload + partFile/save 各挂 1 张 canvas 画的配件简笔图（800×600 PNG，带「模拟图片」角标，sys_file 40–59），自动成为主图；发动机支架胶垫原本就有图（零件 63）未动。未改代码
- 2026-09-23：静态资源加版本号，改完 js/css 不用再 Ctrl+F5。新增 `model/StaticAsset.php`（`StaticAsset::url('/js/lib/dxGrid.js')` = staticUrl + 路径 + `?v=filemtime`，同一请求内缓存），26 个视图 52 处 js/css 引用全部换成它（含 jQuery / DevExtreme / FontAwesome 引用，第三方文件本身未动）；只改 pdc，规则写进 UI.md「静态资源引用」。php -l 全过，浏览器实测登录页 / 后台框架 / 零件列表资源都带 `?v=`、无控制台错误
- 2026-09-23：零件列表「图片」列加列头筛选「有图片 / 无图片」（headerFilter 写死两项过滤表达式 `part_item_lnk_sys_file_id <> null` / `= null`，不请求后端分组），只改 `partItem/index.view.php`；浏览器实测 有 2 / 无 500 / 全选 502，只留「无」时 DevExtreme 转成排除模式 `!(<> null)` 结果一致。语言包 +2 已译 en（扫描同时收进别处 1 条未译 key，非本次）
- 2026-09-23：修复列表列头筛选器候选值全是「(空白)」（跨层）。根因：`dxGrid.js` 没把 `group` 传给后端、框架 `Model::grid()` 也不认 `group`，DevExtreme 把普通行当分组读，key 全空。框架新增 `Model::gridGroups()`（支持多级、日期 groupInterval、数值步长、requireGroupCount，带 `gridGroupBy()` 的模型走子查询）+ `gridLoadOptions()` 钩子，`GridActions::grid()` 带 group 时分发过去；`Users`/`Member` 的敏感列校验、`WashSheetItem` 的清洗单范围移进钩子，敏感列校验增加 group。零件列表浏览器实测分类筛选、日期分组、拖列分组都正常。**静态 JS 无版本号，用户需强刷才能拿到新 dxGrid.js**
- 2026-09-23：按用户要求造模拟零件数据（本次会话用户授权直接写 192.168.1.188 / pdc）：20 个末级分类 × 25 = 500 个零件（ID 64–563），备注统一「模拟数据 2026-09-23」可按备注清理；走业务 Model 写入，号码 1000 条（主号码 + OE 号）、检索 500 行、审核日志 399 条，未改代码
- 2026-09-23：零件详情「文件」/「图片」分页里非图片文件改成按扩展名显示类型图标（内联 SVG 折角文档 + 中间写扩展名，PDF 红 / 表格绿 / 文档蓝 / 演示橙 / 视频紫 / 图纸青 / 压缩包橙 / 其它灰），替换原来统一的 `dx-icon-doc` 灰块；没列到的扩展名也能看出是什么（图标里就是扩展名本身）。只改 `detail.view.php` + `partItem.css`，无新增依赖、无语言包改动，浏览器实测 xlsx / pdf 正常
- 2026-09-22：数据库从 192.168.1.42（MySQL 5.7.18）切到 192.168.1.188（MySQL 8.4.8），按用户要求把 .42 的 `pdc` 库整库同步到 .188（67 张表按 .42 的 DDL 重建 + 全量拷数据，外键规则显式补回 RESTRICT）；逐行对比 67 张表内容、AUTO_INCREMENT、结构全部一致。.188 上被覆盖的旧行已备份到会话 scratchpad。号码保存在 8.4 上正常（5.7 的 `INSERT … AS new` 问题随之消失），数据来源规则接口复测通过
- 2026-09-22：零件数据来源收口（`sql/migrate_part_data_source.sql` 经用户授权已执行：号码 2 / 文件 2 / 车型 10 行改人工，字典停用 9 项）。规则：只有清洗转零件带进来的才是 TCD / EPC（中国车型保留宜配），其余一律「人工」，不留空；页面可改但只能三选一（车型多宜配）。新 Model `PartDataSource`（options / idOf / normalize），号码 / 文件 / 车型 / 关联四个写入口和转零件都走它，明细本身号码从 system 改 manual；详情页下拉去掉清空、号码新行和关联弹窗默认人工。接口实测：非法来源（系统 / 号码上的宜配）被拒、空值存成人工、车型可选宜配、弹窗加车型默认人工，测试数据已删。**号码保存在本机库（MySQL 5.7.18）上被 `PartItemSearch` 的 `INSERT … AS new` 语法挡住（需 8.0.19+，原有问题，已另开任务）**。语言包 +1 已译
- 2026-09-22：零件详情「适用车型」的新增改成参考系统 `oe/listModel` / `oe/listCnModel` 同款：国际 / 中国车型库各一个子分页、表格只列本库已挂的车型，工具条「新增」弹 98% 大窗（确定 + 全部 / 首字母栏 + 整库车型远程分页表格，已挂的预先勾上，确定只提交新勾的）；去掉原来的层级树 + 关键字搜索 + 候选表。新接口 `carVehicle/letters` / `carVehicle/pick`（替换掉 `carVehicle/grid`（按节点）/ `search`，`CarVehicle::search()`、`scopeToNode()` 删除）、`partVehicle/vehicleIds`，`partVehicle/grid` 可带 `libraryId`。浏览器实测两库新增 / 跨字母勾选 / 预勾选通过，测试数据已删；语言包 +2 / −7，en 已译
- 2026-09-22：适用车型两个车型库的表格加「适用品牌」列（`car_brand_names`）：`PartVehicle::grid()` 按当前页一次换算——`CarNode::topIdsByNodeIds()`（取 `car_node_path` 第一段）→ `CarBrand::namesByTopNodeIds()`，口径同顶部「适用品牌」，不能过滤排序。接口 + 浏览器实测国际（众泰 / 奥迪 / 风神）、中国（本田）正确
- 2026-09-22：适用车型表格「所属」改名「车系」；「适用品牌」「车系」改成 SQL 计算列（`PartVehicle::gridComputedColumns()`：联 `car_node` 取直属上级名；品牌用子查询按 `car_node_path` 第一段 → `car_brand_node` → `Lang::translation` 显示名 GROUP_CONCAT），可过滤排序；上一条加的 `CarNode::topIdsByNodeIds()` / `CarBrand::namesByTopNodeIds()` 和不再使用的 `CarNode::nameById()` 删除。接口 + 页面过滤实测通过，语言包 +1（车系，en Series）/ −1（所属）
- 2026-09-22：分类列表一级分类（`part_category_parent_id` 为空）名称改成粗体（`.pc-root-name`）、点击只展开 / 收起下级，双击行也不开详情；一级分类的详情只能选中后点「修改」打开。下级分类照旧点名称 / 双击开详情。只改 `view/partCategory/index.view.php` + `css/partCategory.css`，无文案变化。浏览器实测：点一级名称展开 / 再点收起、不新开标签，「修改」能开一级详情，下级链接正常

- 2026-09-22：分类详情页改成零件详情同款排版 + TecDoc 名称维护。页头「#ID 编码 名称」+ 右侧保存（没改动时禁用、Ctrl+S、「基本信息」分页标题打橙点）；下面分页「基本信息 / TecDoc名称 / 参数（末级才有）」。基本信息套 `mdm-industrial` 紧凑皮肤 + 左侧标签（`partCategoryFormItems` 加 `compact` 压矮多行框），安装位置（末级）并进基本信息、**不再有单独的保存按钮**，和表单一起由页头保存（`partCategoryPosition/save` 接口没变），其它语言翻译照旧行内保存。新增 Model `PartCategoryTcdGa` + Action `partCategoryTcdGa`（GridActions，`categoryId` 限定范围）：表格里直接增删改 GA ID / TecDoc名称 / 中文名，同分类 GA ID 不能重复、GA ID 必须正整数；改完通知列表页刷新 TecDoc 名称列。表 `part_category_tcd_ga` 原来就有，**无 DDL**。实测：未保存标记 / 保存、接口增改删 + 重复 / 非法 / 越权三种报错、表格新增删除均正常，测试数据已清。语言包新增 9 条（en 已译、ru 占位）

- 2026-09-22：分类列表关掉列头排序（`sorting.mode:'none'`，树固定按层级 + 排序值），新增列：是否发动机相关（是的显示「发动机」）、TecDoc名称、号码确认率(%)、车型确认率(%)、默认产品经理、默认采购经理（角色 lookup，可在过滤行按角色筛）。TecDoc 名称来自 `part_category_tcd_ga`（`PartCategory::tree()` 里 `tcdGaNames()` 一次查全表、按分类归并，英文名缓存优先、没有用中文名），名称自带逗号所以多个之间用「; 」隔开，单元格截断、悬停看全部；中文名 / 英文名给了 `minWidth`，窄屏横向滚动。无 DDL。浏览器实测：361 个分类有 TecDoc 映射，列显示 / 过滤正常；本地数据目前没有标发动机和配产品 / 采购经理的分类，这几列暂时是空的。语言包新增「TecDoc名称」（en 已译、ru 占位）

- 2026-09-22：分类列表树默认收拢 + 「全部展开 / 全部收起」切换按钮（工具条第 4 项，`toggleExpandAll()` 改 `expandedRowKeys`）；有过滤（过滤行 / 搜索框）时靠 `expandNodesOnFiltering` 自动展开命中节点及其上级，清掉过滤回到原展开状态。只改 `view/partCategory/index.view.php`，文案复用已有的「全部展开 / 全部收起」，语言包不用动。浏览器实测：默认只显示 21 个根分类，展开 / 收起切换正常，过滤「刹车」→ 制动系展开出刹车线 / 刹车钳 / 刹车盘，清空后回到收拢

- 2026-09-22：零件分类详情改成在后台多标签里打开。新增 `partCategory/detail`（`categoryId`，页头编码 + 名称 + 保存 / Ctrl+S，下面依次基本信息、其它语言翻译、末级才有的参数管理 / 安装位置管理）+ `PartCategory::find()`；分类树点中文名（链接）、双击行、「修改」都走 `openPartCategoryDetail()` 开标签，原来的大编辑弹窗删了。「新增」仍是弹窗、只填基本信息，保存后直接开新分类的详情标签。两页共用的表单抽到 `static/js/lib/partCategory.js` + `view/partCategory/formText.view.php`（字段文案）；详情保存后借 localStorage `storage` 事件通知「分类」标签的树刷新。无 DDL、接口没改。浏览器实测：点名称 / 「修改」开标签、末级四个区块齐、改排序保存后树即时变化（已改回）、新增弹窗默认挂在选中分类下。语言包新增「分类详情」（en 已译、ru 占位）

- 2026-09-22：零件列表页去掉「新增」+ 分类多选支持父子联动。① **列表页不再有新增入口**（用户定：零件从数据单转换而来），删掉新增弹窗 `openCreatePopup()` 和建完打开详情的 `afterCreated()`，表格工具条 `custom` 清空，只给新增用的 `vendors` 注入和 4 条文案（新增 / 中文名称 / 主号码厂商 / 号码重复提示…）一并删掉；服务端 `partItem/save` 无 key 的新增分支保留（接口没删，页面不调），数据单转零件走的是 `PartItem::insertDraft()`，不受影响。② `partCategoryPicker` 新增 `cascade` 选项（默认 false）：勾一个分类连它下面的全勾上、取消全取消，上级按「下面是不是全勾了」自动打钩 / 半选（`toggle()` + 新增 `syncAncestors()`、`syncInput()` 画 indeterminate）。**只有零件列表开了它**，中国车型统计那边保持原样（在那里勾大类是「加一列覆盖率」，级联下去会变成几十列，已实测不受影响）。顺带把面板上的小标签改成只列没被上级覆盖的（勾「制动系」原来会列 29 条，现在 1 条），去掉一条时连它下面的一起去掉。浏览器实测：勾「配气机构」→ 17 个下级全勾（已选 18），取消其中一个 → 上级变半选、再勾回来上级自动全勾，取消上级 → 全清；勾「制动系」确定 → 面板 1 个标签、列表只剩刹车片，点标签 ✕ → 回到 2 条

- 2026-09-22：零件列表页顶部改版。① 去掉关键词搜索框（用户说不需要；服务端 `keyword` 保留，页面不再传）；② 批量类按钮（号码批量查询 / 批量导入图片 / 批量审核 / 批量修改）从表格工具条搬到表格上方单独一行（`#partTopBar` 里的 dxToolbar），表格工具条只留「新增」+ 通用按钮，按钮 onClick 把 `grid` 实例传进去，弹窗里拿到的还是同一个表格；③ 新增「分类」按钮开关左侧分类面板，**面板默认隐藏**（`collapsed` 从 `width:24px` 改成 `display:none`，昨天放在面板头上的收拢箭头一并删掉），开关后照旧 `grid.updateDimensions()`。浏览器实测：默认无面板、表格宽 1184，点「分类」出面板、表格 914，再点收回；号码批量查询查 GDP900 → 只剩刹车片、顶部出现「按号码查询: 1」、点清除恢复 2 条；未勾选点批量审核提示「请先勾选零件」，勾一条后批量修改 / 批量导入图片弹窗正常。语言包：删「名称、号码或分类关键词」「收起分类」「展开分类」，新增「显示或隐藏分类面板」（zh-CN / en 已填，ru 占位）

- 2026-09-21：零件列表页左侧分类面板加「向左收拢」和「多选」。收拢给 `#partCategoryPanel` 加 `collapsed`（260px → 24px，只留展开箭头），切换后调 `grid.updateDimensions()` 让表格重算列宽（实测可用宽度 535 → 771）。多选复用全项目的 `createPartCategoryPicker`（`multiple:true` + `leafOnly:false`，选大类就含下级，口径和点分类树一致），宿主输入框用 CSS `display:none` 藏掉（`hidden` 属性压不过 dxTextBox 自带的 display，第一版界面底下露出来一个输入框），只借它的弹窗；选中的分类在面板上列成小标签，每个可单独去掉。**树的单选和弹窗的多选互斥**，一边有值就清掉另一边。服务端 `PartItem::setListScope()` 第一个参数改成 `int|array`（`scopeCategoryId` → `scopeCategoryIds`，各自展开下级后合并去重），`partItem` 新增 `listCategoryIds()`：多选的 `categoryIds` 优先，没有才用 `categoryId`；导出是表单 POST、数组被前端序列化成 JSON 字符串，所以字符串形态也认。无 DDL。浏览器实测：勾「发动机支架胶垫」+「制动系」两个大类 → 列表正好两条（含下级），去掉一个标签 → 剩一条，点分类树 / 全部分类 → 标签清空；导出 `categoryIds=[4]` 与 `categoryId=4` 的 xlsx 大小一致、都小于不过滤时。语言包新增 2 条（收起分类 / 展开分类），「多选」「全部分类」原来就有

- 2026-09-21：零件详情「文件」分页拆成「图片」「文件」两个（用户要求图片类型单独出来）。过滤放服务端：`PartFile::scopeToTypes()` + `partFile/grid` 新增 `fileType` 参数（`image` = 图片、`file` = 图纸 + 附件，不传仍是全部，zip 批量导入那条路没动）；视图里两个分页共用 `renderFileLikeTab()`，差别只在上传按钮文案、空表提示、标签列（`image_tag` 是图片专用，「文件」分页不显示）和允许的扩展名——**上传时就按扩展名各收各的**（`IMAGE_EXTS` 与 `PartFile::TYPE_BY_EXT` 的 image 一组对齐），免得传完发现文件按类型判到另一个分页去了。类型列两边都保留：判错了改类型，行会挪到另一个分页，所以增删改后两个表格一起刷新（`refreshFileGrids()`，只刷已渲染的，分页是 deferRendering）。CSS 里 `#partFileUploader` 改成 `.part-file-uploader`（两个分页共用），删掉不再用到的 `.part-tag-disabled`。无 DDL。验证：#7（5 张图片）两个分页分别返回 5 / 0 条，全部类型仍是 5 条，`fileType` 传非法值按「不限」处理；「图片」分页有「图片标签」列、扩展名只剩 6 个图片格式，「文件」分页无标签列、扩展名是站点允许的其余 16 个。**没有图纸 / 附件的真实数据可测**（本机 MySQL 处在 `innodb_force_recovery > 0`，写不进去，建不了测试数据），「文件」分页只验到空表状态。语言包新增 4 条待扫描翻译

- 2026-09-21：站点 DEBUG 开关 + ajax 异常回 JSON（**跨层**）。`config/admin.php` 新增 `DEBUG`（bool，默认 true），入口 `admin/index.php` 不再写死 `define('DEBUG', true)`，改成读这份配置（要在 require Mvc.php 之后读，菜单里的 `lg()` 此刻原样返回、不影响取开关）。框架侧 `ExceptionHandler::handle()` 改了出口：异常一律先记日志；带 `X-Requested-With` 或 JSON 请求/接受的按统一格式回 `code=500` 的 JSON，页面请求维持原样。**业务层解决不了**——PDOException 在 Medoo 深处抛出，Action 接不到，全局处理器是框架注册的。`msg` 详略由 DEBUG 决定：开＝异常原文 + `文件:行号`，关＝「系统出现异常错误」。起因是零件详情点保存只弹一个 `Error`：真实原因是本机 MySQL 处在 `innodb_force_recovery > 0`（只读，写不进去），以前异常处理器回的是 HTML，前端解析不了就退化成那句 `Error`。验证：同一个 save 请求 DEBUG=true 时 `msg` 是 `SQLSTATE[HY000]: General error: 1881 ...`、界面弹的就是这句，DEBUG=false 时只剩「系统出现异常错误」，两次 Content-Type 都是 application/json；契约已更新（API.md 错误码表加 500、框架契约加 ExceptionHandler 一节，AGENTS.md 同步）

- 2026-09-20：零件详情「基本信息」改工业风紧凑表单（只动这一个分页，其它分页没动）。通用皮肤 `.mdm-industrial` 写在 `dxCustom.css`：白底输入框、1px 实线边框 + 3px 小圆角、去掉 material 的下划线和焦点动画、表单行距 10px→2px（material 的 20px 上边距那条得照抄它的 `:not()` 串才压得住）、每行下边距 10px→2px、列间距 20px→10px、输入区上下留白 9px→1px，控件高度统一 22px（`.dx-button` 自带 `height:28px`、按钮里图标的行框会把带按钮的输入框顶到 30px，都要一起压），标签右对齐；本页的分区排版（段落标题、安装位置/来源行距、分类选择器不按只读灰底画）在 `partItem.css` 的 `.part-basic`。视图侧只加了容器 class 和把三个文本域高度 70/70/60 收到 44/44/40。输入框字号仍是 11pt（UI 契约的最小字号，没有下调）。皮肤按容器 class 生效，别的页面不受影响。验证：本机 1400px 宽打开 #63 详情页截图确认——控件高度 22px、基本信息整块从 411px 压到 223px，分类选择器仍是白底可点。顺带处理未保存圆点把分页标题顶错位的问题：`.dx-tab-text` 是纵向 flex 容器，行内 ::after 被当成 flex item 掉到标题下面（标签从 34px 撑到 51px）；先改成绝对定位，用户那边仍不满意，**按用户决定整个去掉圆点**（删 `TAB_DIRTY_KEYS`、`refreshSaveUi()` 里打 class 的那段和 `.part-tab-dirty` 样式，契约同步），有没有改动看页头保存按钮是否可用。实测改「开发状态」前后六个标签都是 34px 同基线、保存按钮照常变可用

- 2026-09-20：清洗详情页去掉替换号（用户定：这里不参与）——卡片里的那一行勾选框和 `renderReplace()` / `.wash-replace` 样式一起删掉。`selection.replace` 这一组**保留**（转零件还要用），`currentSelection()` 里原样带回库里现在的值，否则保存勾选会把替换号清成空（后端 `saveSelection()` 缺组就当没勾）。「深度清洗」列里的替换号条数不动，只是个计数。验证：样例页无替换号区块、标签仍 7 项，保存勾选提交 numbers 76 / replace 29（未动）/ intl 1541 / cn 174
- 2026-09-20：替换号挪进产品卡片内（上一条放在卡片下面单独一行，用户要求收进卡片）；顺带核对号码三个分组的逻辑——分组口径（`num`/`othnum` × `ok`）和参考系统一致，但原来 `fillFromTcd()` 是**跨组**按 unikey 去重，同一号码在 num 里出现过就会被 othnum 丢掉（参考系统两个列表各渲染各的），改成各组内去重、`key` 记成 `组名:unikey`。用户说「缺少确定的其它号码」查到根因不是逻辑：拿参考系统 `wdi_id=135528`（确认产品 RICAMBIFLEX GM:01620964 / art 22538792、小类 62 清洗率 70/50）对照，MDM 按同一产品同一清洗率算出 8/0/1/5，参考系统存的快照是 8/0/1/4，差的那 1 条是上游后来新增的 `TWS:VL2149`（参考系统那份是旧快照）；而用户在 MDM 打开的明细确认的是 SWAG 33 11 0845，上游对它 `othnum` 返回 0 条，所以两个「其它号码」标签本来就不显示。验证：item 11 / item 1 用新代码算出的四组条数与上游按组去重后的条数完全一致（56/45/20/303、2/15/0/0），key 全唯一；样例页替换号 29 个勾选框在卡片内，标签仍是 7 项
- 2026-09-20：清洗详情的标签对齐参考系统——「替换号」不该是一个标签页。它是 OE 替换号（`etk/getReplaceNumber` 查出来的），参考系统里是清洗数据上面的一行 `OE替换号：xxx, yyy`，所以移到产品卡片下面一行显示，每个号码一个勾选框（转零件要用勾选结果，不能退化成纯文本）。这一行对外仍然装成一个「可勾选的表格」（自带 `getSelectedRowKeys` / `selectRows`）进 `grids`，保存勾选 / 恢复建议勾选的代码一行没改。标签页现在只剩：确定的对照号 / 不确定的对照号 / 确定的其它号码 / 不确定的其它号码（空组不显示）+ 产品国际车型 / 确定号码的国际车型 / 中国车型，和参考系统一致。验证：样例页标签正好是这 7 项（参考系统截图里是 6 项，因为那条明细「不确定的对照号」为 0）；替换号 29 个勾选框，取消 3 个后「保存勾选」提交 replace=26、「恢复建议勾选」回到 29，numbers / vehicles 三组不受影响
- 2026-09-20：修号码比较三个实测问题（用户拿悬置支架那条明细跑出来的）——① **`ga_id` 不再默认传**：传了之后 `tcd/compareNum` 把每列收敛到同产品类型下的交集，5 个号码每列都只剩同样 2 条；不传则是 3/17/11/12/3 条。改成 `sameCategory` 参数（默认 false），弹窗顶部加「只比同类型」复选框，勾了才传、切换即重查。② 弹窗标题重复「比较已选号码 · 比较已选号码」——按钮不再把标题当后缀传。③ 按号码比（type=out）接口不返回图片 / 参数（只有 type=in 才有，之前的样例截图用的是 in 的数据，所以看着更丰富），现在用详情页清洗结果里已有的号码图片按「厂商:号码」（去空格大写）补到列上，不额外请求。验证：CLI 对同一组号码 `sameCategory=false` → 5 列 26 行（2 行全有、8 行部分有）、`true` → 2 行；样例页标题只剩一个、复选框切换发出的 `sameCategory` 依次是 false/true、列数随之从 443/118/224 变 377/95/198，接口 0 张图时本地补出 2 张。语言包再加 2 条
- 2026-09-20：清洗详情补检测依据的匹配率 + 号码比较——号码项的 `checks` 从字符串数组改成 `[{type, msg, rate}]`（顺带存 `art_id`），界面显示成「[3]各自搜索出来的号码 80.98%」，其中 `outcom`（和确认产品按号码比）和 `refnumrate`（两个 TCD 产品按对照号比）是链接，点开号码比较；新增「比较已选号码」按钮（收四个号码标签里勾选的号码，OE 号厂商统一用 OE，至少 2 个）。新端点 `washSheetItem/compareNumbers` → `WashSheetItem::compareNumbers()` → `Wash::compareNumbers()` 调 ypAPI `tcd/compareNum`（**这个接口的参数要整包 json 塞进表单字段 `data`**，铺平成普通表单字段不认，和其它 ypAPI 不一样）；对齐逻辑在后端（每个号码一列、按对照号 unikey 对齐成行，全都有的 same=all 标绿、部分有的 same=some 标蓝，行按共有数量排序）。旧结果的 checks 是纯字符串，前端两种都认（只是没有匹配率和链接）。验证：CLI 跑 `out`（2 个号码 → 2 列 387 行、85 行全有）和 `in`（2 个 art_id → 2 列 196 行、带图片和 10 条参数），参数校验（少于 2 个号码 / type 不合法）都拦住；样例页里检测依据显示 `[0]产品主号码` `[3]各自搜索出来的号码 80.98%`，点链接发出的是 `type=out` + 确认产品号码 + 本行号码，「比较已选号码」收到 76 个勾选号码，比较表格 2 列 199 行、27 行标绿渲染正常。语言包新增 4 条
- 2026-09-20：清洗详情的号码表格补「图片」列——`fillFromTcd()` 的号码项存 `pic` / `pic_big`（接口一个号码可能给多张，只取第一张的 small / big），EPC 的 OE 号没图、留空串保持结构一致；四个号码标签共用的 `numberColumns()` 第一列改成缩略图（`.wash-num-pic`，点开大图，`washSafeUrl()` 过滤）。结果体积：刹车片这条 424 个号码、341 个有图，压缩后 30KB → 69KB，仍在「几十 KB」量级。`图片` 语言包里已有，不用加词条。旧结果没有 pic，这列是空的，要重新深度清洗。验证：只读重建结果喂样例页，`确定的对照号` 首页 22 张图渲染出来（首图 120×80 缩到 60 高）
- 2026-09-20：清洗详情补「确定号码的国际车型」标签（上一条里说「拆不回来」的那项，改成清洗时记来源就能拆）——`WashSheetItemClean` 的车型项新增 `sources`（`model` 产品车型 / `model_oknum` 确定号码推出来的车型，两边都有的车型仍只存一条、sources 记两个），统计加 `vehicle_intl_product` / `vehicle_intl_oknum`；详情页国际车型分成「产品国际车型」「确定号码的国际车型」两个标签，**勾选仍是 `vehicles_intl` 一组**（两标签取并集，后端 `saveSelection()` 已 `array_unique`）。旧结果没有 `sources`，一律当产品车型只出一个标签，要分开得重新深度清洗。验证：只读调 `tcd/cleanData`（art_id 2706912）确认上游 model=766 / model_oknum=1524、交集 766、合并 1524 与库里一致；再用反射调 `buildResult()`+`attachVehicleInfo()` 在不写库的前提下生成带 sources 的结果喂样例页，标签页显示`确定的对照号 56/56 · 不确定 0/45 · 确定的其它号码 20/20 · 不确定 0/303 · 替换号 29/29 · 产品国际车型 766/766 · 确定号码的国际车型 775/1524 · 中国车型 174/174`。语言包再加 2 条
- 2026-09-20：清洗详情从弹窗改成页面 `washSheetItem/detail/k/itemId/{ID}`（`AdminLayout.openTab` 开后台新标签，像零件详情那样），原弹窗删除，明细列表的「已确认数据」号码和「深度清洗」的「清洗详情」两个入口都指向它；原 JSON 端点 `washSheetItem/detail` 改名 `info`（对齐 partItem 的 detail 页 / info 数据）。同时按参考系统 catalog_frey `work/wdiDetail` 补内容：产品卡片（图片 + 「厂商: 号码」链接 + 产品类型中英文 / EPC 零件组与位置 + 中英文参数两列，参数不再单独占标签页）、号码按「对照号 / 其它号码」×「确定 / 不确定」拆四个标签（空的不显示，列：厂商/汽车商、号码、类型 OE、建议、检测依据）、车型表格补齐 马力 / 排量 / 燃料 / 缸数 / 驱动 / 车身（`attachVehicleInfo()` 多取这 5 列）、头部加「零件」行（已转零件链接 + 「转零件」入口，复用 `/toPart`）。勾选提交改成按组合并（号码来自四个表格）。**没做**：号码对应的「已有零件」列（参考系统的 poes，需要新后端查询）。离线样例页（真实 DevExtreme + CSS + 真实清洗结果 JSON）验过 TCD（1524 车型 / 424 号码）和 EPC 两条明细：四个号码标签计数正确、车型 15 列都有值、EPC 卡片显示零件组和位置。语言包新增 11 条（en 已译、ru 占位），**langTool 扫描和后台实测未做**（要登录；本机 80 端口能跑 MDM，8087 是 PHP 7 只能跑 catalog_frey）
- 2026-09-20：数据单明细新增「已确认数据」列（对齐参考系统 catalog_frey `work/dataItemList` 的 `chkok` 列）——显示确认的「厂商: 号码」+ 深度清洗出的参数，号码点开本系统的清洗详情（同日后续改成了页面 `washSheetItem/detail`，见上一条），旁边一个小图标是上游产品页；未确认 / 未深度清洗 / 结果过期都有对应提示。参数来自 `wash_sheet_item_clean_summary`（`summarise()` 新增 `params`），因为完整结果是压缩 BLOB，列表不许读；已有 5 行用新增的 `WashSheetItemClean::resummarise()` 经用户授权回填（只更新 summary 列，不调远端接口）。改 `washClean.js`（`renderConfirmed()`，列排在 EPC 与分类之间）/ `washCleanText.view.php` / `washSheet.css` / `WashSheetItemClean.php`，两个明细页（`washSheetItem/index`、`washSheet/detail`）共用这份列定义，页面文件没动。无 DDL，语言包新增 2 条（已确认数据 / 产品页面，en 已译、ru 占位），**langTool 扫描和后台页面实测未做**（要登录）；已用静态样例页在 240px 列宽下验证 5 种状态不裁切、长参数值换行正常
- 2026-09-20：弹窗/表单编辑里的布尔字段（复选框）标签与框挤成两行的问题，统一改成一行——新增全局 `js/lib/dxDefaults.js`（在 `public/header.view.php` 里紧跟 DevExtreme 之后引入），用 `dxDataGrid`/`dxTreeList` 的 `defaultOptions` 挂上 `editing.form.customizeItem`，给 `dataType: 'boolean'` 的表单项加 `mdm-field-inline-check` class（页面自己写了 `label.visible: false` 的不插手），排版写在 `dxCustom.css`：`flex-direction: row-reverse` 让复选框在左、标签在右。不改第三方源码、不改任何页面，车系（乘用车/商用车）、属性类型、启用位、是否主号等 20 来处布尔列一次全生效。用本地静态样例页（DevExtreme + dxDefaults.js + 含布尔列的弹窗编辑 grid）在浏览器里实测：两个布尔项都排成一行，无控制台报错；**后台页面实测待人工**（要登录）
- 2026-09-20：零件分类选择弹窗改交互——弹窗宽高由 80% 改成 100%（铺满，`maxWidth` 去掉）；**单选不再用 radio**，分类渲染成链接（`.pcp-link`），点一下直接选中并关窗，底部只留「取消」，当前已选项加粗 + `#CCE4F6` 底色标出；**多选维持原样**：勾选框 + 全选 + 已选计数 + 「确定」才生效。查询分页同理（单选整行是链接，下划线只给分类名）。只改 `lib/partCategoryPicker.js` + `css/partCategoryPicker.css`，`createPartCategoryPicker()` 参数没变，5 处调用点不用动；语言包 `pickFirst`（请先选择分类）已无用，从 `partCategoryPickerText.view.php` 移除。**浏览器实测待人工**（后台需登录）
- 2026-09-20：汽车品牌「匹配关键字」改为后端自动生成——`CarBrand::refreshKeyword()` 按 中文名 + 英文名 + `car_brand_lang` 译名 拼出，品牌保存和译文增删改都会刷新，列从列表和编辑框里去掉，不再人工维护；存量 133 行已用户授权后重算（32 行有变，其中 28 行原本为空，东风的 `dfc` 按拍板改成 `东风,east wind`），回滚 SQL 已导出。CLI 验证新增 / 改名 / 加译名 / 删译名 四条路径都能自动刷新，测试品牌已删
- 2026-09-20：汽车品牌列表精简，去掉「排序」「映射车型节点数」「号码引用数」三列（列表 + 编辑框都不露出，`car_brand_node_count` / `car_brand_number_count` 两个标量子查询从 `gridComputedColumns()` 删掉）；`car_brand_sort` 改成隐藏且不进列选择器的列，只负责让请求带默认排序，新增时取 `max+1`。CLI 验证 `grid()` 正常（133 行），浏览器实测待人工
- 2026-09-19：汽车品牌「匹配关键字」去掉 `%`——旧库 `tag_brand.tb_words` 按 SQL LIKE 模式存（`%宝马%,%bmw%`），迁移原样搬入，133 行里 104 行带 `%`；`part_category_keyword` 查过无 `%`。`CarBrand::cleanKeyword()` 保存时去 `%`、全角逗号转半角、去空去重，迁移脚本同样处理。存量数据已经用户授权后清洗（104 行，事务内执行，回滚 SQL 已导出）

- 2026-09-19：排查所有定义了 `gridDefaultOrder()` 的 Model 是否被 DevExtreme 自动补的「按主键正序」盖掉。受影响并已在列上加 `sortOrder`/`sortIndex`：汽车品牌、零件列表、零件详情的号码 / 文件 / 适用车型 / 关联（后三个补了隐藏的排序列 / ID 列）；不受影响：分类参数（本地模式，请求不带 sort）、PartParamOption（无页面）、SysRegion（无 Action）。零件列表的关键词只做过滤，原本就没有相关度排序，没什么可丢。**顺带修 `dxGrid.js`**：「重置」「恢复布局」原来 `clearSorting()` 会把列上的默认排序也清掉、又退回主键正序，改成恢复列声明的默认排序。浏览器逐页实测通过，控制台无报错

- 2026-09-19：新增「国际车系」菜单（参考 catalog_frey `carmodel/arealist`：左车厂 + 右车系表格）。新 Model `CarSeries` / Action `carSeriesIntl` / 视图 `carSeries/index`，无 DDL。和参考系统的差异：名称不做唯一校验（现有数据 2 万多车系里同车厂同名很常见）、来源 ID 可不填自动生成 `M{ID}`、乘用 / 商用可编辑、可换所属车厂、左边多一列英文名（车厂名是中文）。CLI 21 项 + 浏览器实测通过，语言包 703 条。**坑**：远程分页没指定排序时 DevExtreme 自动补「按主键排序」，Model 的 gridDefaultOrder() 不生效——默认排序要写在列的 sortOrder/sortIndex 上（同会话已顺带修好国际 / 中国车型页：ID 列 `sortOrder: 'desc'`，浏览器实测通过）

- 2026-09-19：零件分类选择统一改成「点击弹窗选 + 查询」（参考系统 catalog_frey 的「选择小类」弹窗）。新增共用控件 `lib/partCategoryPicker.js` + `css/partCategoryPicker.css` + `public/partCategoryPickerText.view.php`，弹窗内两个分页：「选择分类」按层级把分类全铺开打钩（大类一块、中类一行、末级横排，几百个条目用原生 input 而不是 dxCheckBox，避免卡顿），「查询分类」关键词匹配名称 / 编码 / 上级路径、结果显示完整路径，两个分页共用一份勾选状态。单选是 radio，多选是 checkbox + 「全选/全不选」+ 已选计数；`leafOnly:false` 时任意一级都能选且互不联动（中国车型统计用）。5 处调用点全部换掉，删掉 `partItem.js` 的 `partCategorySelectOptions()` 和统计页的 dxDropDownBox + dxTreeView。**坑**：放进 dxForm 只能当 `template` 项，`validationRules` 和 `readOnly` 都不生效——必填改成保存前自己判 + `markInvalid()`，星号用 `isRequired: true`，只读态用 `setDisabled()`，值变了要自己写回 `form.option('formData')`；搜索框 `valueChangeEvent` 只写 `keyup` 时中文输入法和粘贴会漏，改成 `keyup input change`。无 DDL、无接口改动，浏览器 5 处实测通过，语言包 686 条

- 2026-09-19：中国车型「同步车型」+ 同步进度条。接口和令牌沿用参考系统 catalog_frey 的 `YPAPI`，配置落在 `config/common.php` 的 `ypModelAPI`。新 Model `CarVehicleSync`：拉全量 → 按祖先链还原成 `car_node` 四层 → 逐层增删改 → 再比对 `car_vehicle`，被零件引用的车型和非空节点一律不删。进度条是真实百分比（服务端按阶段写进度文件，前端每秒轮询）。**事故与教训**：浏览器实测期间有一次真正写库的同步被跑掉了（我原计划先问用户再执行），事后完整性校验全过、结果与上游一致；随后的预览又撞上接口返回 `{status,data,code,msg}` 状态包（`count()`=4）、代码把它当成 4 行车型、差点提出删掉全库 8.3 万款——**已补两道安全闸**（响应必须是车型列表、行数不得低于现有的 90%），并补了 12 项针对性自测。另踩两个坑：祖先链（只按 `make_id` 认生产商会串品牌，节点新增数从虚高的 1937 降到正确的 278）、dxPopup 把内容搬进 overlay 容器（按外层 id 反查拿不到）。顺带发现参考系统 `uploadModel` 漏映射 `mod3_drive`，MDM 这次补上了

- 2026-09-19：修复「同步车型」报「同步失败：」（后面一片空白）。两个独立原因：① `config/common.php` 的 `ypModelAPI.token` 被换成了清洗接口的 `YPPDC-tesT-…`，车型接口不认，返回 `{status:false,data:"token不存在",code:"token"}`——正是安全闸设计时预想的那种状态包，闸正常拦下、一行没改，已改回 `FREY-WEB1-…` 并在配置和 `API.md` 写明两个令牌不通用；② 闸抛的 `MvcException` 没人接，冒到全局处理器输出 HTML，前端按 JSON 解析拿不到 `msg`，所以「同步失败：」后面是空的——`syncRun` / `syncProgress` / `syncReport` 改成自己 catch 后走 `$this->error()`（`error()` 内部 exit，`finally` 不执行，所以先记文案、收完进度文件再报错），前端再兜一层非 JSON 响应。改完 CLI 预览全量通过（83367 行，各项变更计数全 0，库与上游一致），语言包 675 条

- 2026-09-19：车型菜单三级化 + 「中国车型统计」。`config/admin.php` 把汽车品牌 / 国际车型 / 中国车型收进「零件库 → 车型」下（菜单渲染本来就支持嵌套，没改前端）；参考 catalog_frey 的 `carmodelcn/report` 做了品牌覆盖率报表——顶部分类树多选（可选任意一级，选大类含其下级），每个选中分类一列显示「覆盖率% (已覆盖车型数)」，支持排序 / 过滤 / xlsx 导出。新增 `CarVehicle::brandCoverageReport()` / `topNodeLevelId()` / `brandVehicleCounts()` / `coveredVehicleCounts()`，`carVehicleCn` 加 `report` / `reportData` / `reportExport`。无 DDL，浏览器实测通过，语言包新增 9 条。同会话内用户拍板「所属车型品牌」= 已有的「汽车品牌」，不新建字典也不加菜单——复核发现参考库的 `tag_brand.tb_from_ids` 注释写明它就是 `cd_make` 的合并结果，MDM 的 `car_brand` 拿的已经是合并后的数据，**不需要任何 DDL**（最初以为要补的「拼音 / 别名」两列实为「俄语 / 阿拉伯」，MDM 已有 `car_brand_lang` 承载）。真实缺口是译文没迁：`car_brand_lang` 0 行而 `ru` 已启用，出 `sql/migrate_car_brand_lang_ru.sql`（v1.0.8，18 条 DML，待人工执行）

- 2026-09-20：数据单明细「已确认·人工」标签的位置和截断问题——标签原先挂在 TCD 分类名后面（`$group` 里），既不是它确认的对象，分类名一长还会顶出列宽被裁掉（Grid 默认 `wordWrapEnabled: false`，单元格 nowrap + overflow hidden）。改成挂到被确认的那条号码行上（`isChecked` 的 `.wash-num-line` 里），分类名改为加粗（`.wash-tcd-name-checked`）提示确认项在这个分类下；CSS 给 `.wash-tcd/.wash-epc/.wash-deep/.wash-category` 恢复 `white-space: normal`（列内换行，不再裁切）、`.wash-num-line` 加 `flex-wrap`、标签 `flex-shrink:0 + nowrap` 保证自身不被压断。只改 `washClean.js` 的 `renderTcd()` + `washSheet.css`。用静态样例页在 260px 列宽下量过：超长分类名换两行不溢出，号码 + 标签放不下时标签整块换行，均未被裁；**后台页面实测未做**（要登录）

- 2026-09-20：数据单明细取消编辑功能。原先弹窗编辑表单里 TCD / EPC / 深度清洗三列显示成 `[object Object]`（列只配了 `cellTemplate`，DevExtreme 在表单里走 `editCellTemplate`，没配就把对象丢给默认编辑器）；与用户确认后不做表单只读渲染，直接关掉编辑——明细的号码来自导入、清洗结果由接口写，内容一律在列表里看和管（TCD / EPC 确认单选、分类列弹窗、深度清洗详情、工具条四个批量按钮）。两个页面（`washSheetItem/index`、`washSheet/detail`）的 `gridOptions.editing` 改成只留删除，工具条 `editMode: false` 隐藏「编辑方式」下拉。`washClean.js` / `washCleanText.view.php` / `washSheet.css` 无改动（同会话内先写的表单只读渲染已回退，避免留下不会执行的代码）。列上的 `validationRules` 保留但已不生效。无 DDL、无语言包变动。**浏览器实测未做**

- 2026-09-19：零件适用区域改口径——由适用车型推算，零件本身不设置；删 `PartItemRegion` / `partItemRegion` Action / 详情页区域勾选树，删表 SQL v1.0.6 待人工执行；语言包删 4 条（620）。**车型侧没有区域数据，推算链路和展示位未做**，等数据来源确定

- 2026-09-19：修复超级管理员勾选"记住我"登录报 1452（跨层）。内置超管 `admin_user_id` 固定 0、不入 `admin_user` 表，`admin_remember_token` 的外键把他的令牌挡在外面（SESSION 先写好所以"登录成功了但抛异常"）。改法：去掉该外键（**SQL v1.0.6 `migrate_drop_remember_token_fk.sql` 经用户授权已执行**，含 rollback），级联清理改由 `Users::gridRemove()` 做；`Auth::tryRememberLogin()` 补 id=0 分支让超管的自动登录真正可用；超管账号密码从 `loginAction` 常量移到 `config/common.php` 的 `superAdmin`（本机可 `common.local.php` 覆盖，留空=停用），框架侧新增 `Auth::SUPER_ADMIN_ID` / `Auth::superAdminProfile()`。契约已更新（Auth.md、DB.md 外键一节）。验证：超管勾"记住我"登录不再报 1452、只带 remember_token 请求首页能自动登录且 validator 正常轮换、`Users::gridRemove()` 建临时用户实测能连带清令牌；测试数据已清，令牌表恢复迁移前的 2 行。**浏览器手工实测未做**（走的是 curl 模拟请求）

- 2026-09-19：安装位置语义修正——候选范围完全由分类的「安装位置管理」决定，分类没配置＝该分类的零件没有位置可选（原先是「空＝不限」，与 `part_item_region` 的惯例相反，契约已标注）；无 DDL、无数据迁移；HTTP 15 项 + 浏览器实测通过；语言包新增 2 条已译

- 2026-09-19：零件库 UI 优化第一批（用户从体检清单里挑的 A+B+C 三批）——详情页 9 分页并成 6、页头统一保存 + 未保存圆点、车型改懒加载树 + 输入即搜、关联改远程搜索下拉、文件类型按扩展名自动判定、号码失焦即查重、审核少弹一层窗、列表页批量审核 / 批量修改；无 DDL；HTTP 45 项 + 浏览器实测通过，测试数据已清；语言包 623 条（新增 25 已译、删废弃 13）。**待办**：菜单 6→4 合并、分类页四项改造（弹窗改 TabPanel / 上级分类改树 / 树上拖拽换父级 / 选项批量粘贴）

- 2026-09-17：零件库菜单调整 + 汽车品牌 / 国际车型 / 中国车型三个管理页；号码厂商移入「零件库」、删「零件关联属性」菜单组（配置文件，无 SQL）；CLI 43 项（1 项用例假设错误已复核）+ 浏览器实测通过，测试数据已清；语言包新增 39 条已译

- 2026-09-17：数据单 S6b — 明细转零件；新 Model `WashSheetItemPart` + `toPart` 端点 + 工具条「转零件」/「零件」列；OE 号品牌以文本存 `part_number_car_brand_text`；SQL v1.0.5 经授权执行；CLI 75 项 + 浏览器实测通过，测试数据已清；语言包新增 22 条已译

- 2026-09-17：数据单 S6a — 确认参考产品 / 匹配分类 / 深度清洗；SQL v1.0.4 经授权执行，新增 `WashSheetItemClean`，明细页加分类列、深度清洗列、匹配 / 修改分类、清洗详情弹窗；CLI 38 + 浏览器实测通过，语言包新增 55 条已译（扫描顺带生成了已启用的 `ru.php` 占位文件）；测试数据单 #5 `S6A_CLI_TEST` 留给用户验收后删除

- 2026-09-16：零件分类「安装位置管理」——`partCategory` 编辑弹窗新增「位置管理」面板（按维度分组勾选 `part_position` 叶子值，全量替换），补上 S5 遗留的 `part_category_position` 管理界面缺口；新增 Model `PartCategoryPosition` + Action `partCategoryPosition`。CLI 10 + HTTP 端到端自测通过，语言包新增 3 条已译
- 2026-09-16：零件库 S5 — 关联/区域/安装位置/来源；四个分页 + 5 个新 Model（PartRelation/PartItemRegion/PartItemPosition/PartSource/SysRegion）；CLI 33 + HTTP 端到端自测通过，语言包新增 44 条已译；part_category_position「有配置才限定」的读取逻辑已实现，管理界面未做（留给以后）
- 2026-09-16：零件库 S4 — 适用车型；「适用车型」分页按车型库层级逐级下拉或关键字搜索，勾选候选车型批量加入；CLI 29 + HTTP 端到端自测通过，语言包新增 22 条已译。同会话内用户确认 `part_item_brand` 不需要，改为 `PartVehicle::brandsByItem()` 现查现算并在分页顶部显示「适用品牌」，删表 SQL 待审核；`part_item_summary` 仍未做
- 2026-09-16：零件库 S3 — 图片文件分页 + zip 批量图片；「文件」分页统一管理图片/图纸/附件（分片上传、主文件互斥、图片打标签）；列表页「批量导入图片」按目录式命名全库匹配零件；CLI 36 + HTTP 端到端自测通过，语言包新增 17 条已译
- 2026-09-16：零件库 S2 — 分类管理 + 参数管理 + 参数分页；新增「分类」菜单（dxTreeList）、末级分类挂参数、零件详情页「参数」分页动态表单；CLI 51 + HTTP 全端点自测通过，语言包新增 75 条已译
- 2026-09-16：零件库 S0 — 框架层分片上传 + zip 安全解压（跨层）；CLI 88 + HTTP 43 项自测通过；浏览器实测未做
- 2026-09-16：零件库 S1 — 菜单/列表/新增/基本信息+多语言/号码/审核/删除；CLI 130 + HTTP 45 项自测通过；S2–S5 分页未做，浏览器实测未做
- 2026-09-16：**Token 消耗精简 + Agent 专属文件边界**（跨层，契约维护）。拆分本文件为「现状表 + 一行摘要」与 [modules.archive.md](modules.archive.md)（完整历史记录，54KB → 主文件 ~10KB）；根/pdc/mvc 三层 `AGENTS.md` 补回与 `CLAUDE.md` 同步的内容（根 AGENTS.md 缺失的「第三方代码不可改」一节）并修正指向不存在的 `.Codex/` 目录的断链（改指向实际存在的 `AGENTS.md` / `.claude/contracts` / `.claude/status` 路径）；根契约新增「Agent 专属文件边界」——Claude Code 会话只读 `.claude/CLAUDE.md` 那一份，不读同目录 `AGENTS.md`，反之 Codex 会话不读 `.claude/CLAUDE.md`，避免同一会话把两份镜像内容都读进上下文。未做：还没有找到/接入减少自测输出体积的具体机制，只是原则性建议，留给以后有大批量自测的会话自己把关。
- 2026-09-22：零件详情去掉「来源」区块（界面）——确认来源下拉 + 清洗快照只读行从「基本信息」移除，页面不再调 `partSource/info` / `save`；接口与 `PartSource` 模型保留，数据单转零件仍写快照；浏览器实测通过
- 2026-09-22：零件详情「图片」只收图片、「文件」不限格式（文件分页传的图片记附件）；服务端 `image` 类型校验图片格式、zip 批量导入跳过非图片；上传器扩展名浏览器实测 + CLI 桩测通过，真实上传因数据库只读未做
- 2026-09-22：零件详情记住上次的分页（localStorage 按分页 key 记，刷新 / 换零件都停在原分页，校验跳转不记）；浏览器实测通过
- 2026-09-22：后台顶部标签支持鼠标拖动排序（自研鼠标事件实现，首页标签固定在最左，拖完同步 tabs 数组）；浏览器真实拖动实测通过
- 2026-09-22：后台标签标题截断（CSS 按像素截，当前标签 240px / 其它 160px，完整标题进 title 属性，localStorage 不截）；浏览器实测通过
- 2026-08-31：修复 Model.php PHP 版本兼容问题；规划会话分层与契约体系
- 2026-09-15：框架配合 - 全文检索封装（跨层）；CLI 60 项自测通过，停用词 SQL 后已执行
- 2026-09-15：框架配合 - 语言包按启用语言生成（跨层）；CLI+HTTP+浏览器自测通过
- 2026-09-15：框架配合 - Grid 按多语言显示名筛选排序（跨层）；CLI 46 项自测通过；发现 attr_value 排序规则不一致问题，SQL 后已执行
- 2026-09-15：框架配合 - 通用文件上传（跨层）；CLI 36 + HTTP 43 项自测通过
- 2026-09-07：新增「零件关联属性」菜单组 + numberVendor（号码厂商）模块；表 SQL 后已执行
- 2026-09-07：项目分析（只读），确认生产 PHP 8+，PHP 版本约束下限定为 8.1
- 2026-09-07：新增「数据单」菜单组 + washSheet/washSheetItem 两个模块；表 SQL 后已执行
- 2026-09-07：数据单列表点标题打开明细标签 + 粘贴导入号码；dxGrid.js 加 params/toolbar.custom 通用能力
- 2026-09-07：框架层新增号码去格式函数 `formatNum()` / `getSemiangle()`
- 2026-09-08：修固定列行高错位（`dxSyncRowHeights()`）；新增 `contracts/local.md`；根契约加「第三方代码不可改」
- 2026-09-07：数据清洗上线（`model/Wash.php` 重写为 ypAPI 客户端）；框架 `HttpClient` 加 `postForm()`（跨层）
- 2026-09-15：零件库一期建表与初始数据迁移（45 张新表 + 数据迁移脚本，经授权执行）
- 2026-09-14：零件库结构设计（只读学习参考站，未写代码），出方案草案与命名规则
- 2026-09-14：统一表/字段命名（跨层，大改动）；`admin_user`/`attr_factory` 等改名 + 17 个外键列改名；SQL 经授权已执行，冒烟测试 22 项 + 浏览器验证通过；顺带发现并另开会话修复密码哈希发到浏览器问题
- 2026-09-14：修复密码哈希发到浏览器（`Users`/`Member` 的 `gridSelect()` 用 `u.*`/`m.*` 带出哈希）；冒烟测试 24 项通过，浏览器实测未做
- 2026-09-07：`wash_sheet_item` 增加 `wash_sheet_item_format` 字段（号码格式化值）
