From 021d09f33b6bc44f699449da962358bd5fe3b867 Mon Sep 17 00:00:00 2001 From: zty Date: Fri, 10 Jul 2026 01:53:37 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9D=90=E6=96=99=E5=81=A5=E5=BA=B7?= =?UTF-8?q?=E5=88=86=E7=BA=A7=E4=B8=8E=20Yp=20=E6=8E=92=E5=BA=8F=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ypt 综合值公式 + 全库三分位定 A/B/C 健康档 - 缺值材料不分档、排最后 - 后端共享 material-ranking 工具接入三个列表接口 - 前端两控件(排序依据+分组开关)+ 档标签 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VCShKevM5prWZhp1kk1iGh --- .../2026-07-10-material-sorting-design.md | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 空气质量预测/源码/docs/superpowers/specs/2026-07-10-material-sorting-design.md diff --git a/空气质量预测/源码/docs/superpowers/specs/2026-07-10-material-sorting-design.md b/空气质量预测/源码/docs/superpowers/specs/2026-07-10-material-sorting-design.md new file mode 100644 index 0000000..aaf4f8b --- /dev/null +++ b/空气质量预测/源码/docs/superpowers/specs/2026-07-10-material-sorting-design.md @@ -0,0 +1,126 @@ +# 材料健康分级与 Yp 排序 —— 设计文档 + +日期:2026-07-10 +分支:`feature/source-charts` + +## 1. 背景与目标 + +材料库(公共 + 自建)目前只按手动 `sort_order` 排序,用户无法按"环保程度"挑选材料。本功能让材料能: + +1. 按**健康等级**分成 A/B/C 三档 +2. 按**单个污染物的 Yp 值**(平衡释放量范围,越小越环保)升序排序 —— 可在每档内排,也可全库排 +3. 按**综合值 Ypt**(五种污染物 Yp 的加权和)升序排序 + +**术语对照(已核对源码)**: +- **Yp = `*_be_area`(平衡释放量范围)**。材料表单里甲醛栏标注「平衡释放量范围(Yp)」。 + - Yp甲醛=`methanal_be_area`、YpTVOC=`tvoc_be_area`、Yp苯=`benzene_be_area`、Yp甲苯=`toluene_be_area`、Yp二甲苯=`p_xylene_be_area` +- **健康等级 = `health_level`**,原为自建材料手动维护的 A/B/C 字段;本功能后**不再用于分组**(见 §2)。 + +**成功标准**:材料库/选材界面能按"排序依据 × 是否分组"两个控件排序与分组展示,同一材料在任何界面看到的健康档一致。 + +## 2. 核心计算逻辑 + +### 2.1 Ypt 综合值 + +每个材料一个标量,越小越环保: + +``` +Ypt = methanal_be_area×1.8 + + tvoc_be_area×0.25 + + benzene_be_area×1 + + toluene_be_area×0.45 + + p_xylene_be_area×0.38 +``` + +**缺值规则**:5 个 `*_be_area` 中**任一为空** → 该材料 `ypt = null`、`health_tier = null`,不参与分档,排在所有材料最后。 + +### 2.2 健康档 A/B/C —— 全库三分位 + +- 取**全库所有"数据完整"材料**(公共 + 自建合一),按 Ypt 升序 +- 前 1/3 → **A**(最健康)、中 1/3 → **B**、后 1/3 → **C** +- 边界按**材料个数**切(非按 Ypt 数值区间):n 个完整材料,边界索引为 `⌈n/3⌉`、`⌈2n/3⌉` +- 相同 Ypt 落**同一档**、不强拆(并列时以第一次跨越边界的 Ypt 为准) +- **实时计算,不落库**(分位随库存变化)。现有 `health_level` 手动字段保留但**功能上弃用**(不再用于分组) +- **全库口径**:即使在"项目选材"等子集场景,`health_tier` 也按全库 Ypt 分布来定,不在子集内部重新分位 —— 保证一致性 + +## 3. 后端设计 + +### 3.1 新增共享工具 `src/common/material-ranking.ts` + +纯函数,无 HTTP 依赖,便于单测: + +- `POLL_WEIGHTS`:`{ methanal: 1.8, tvoc: 0.25, benzene: 1, toluene: 0.45, p_xylene: 0.38 }` +- `computeYpt(m): number | null` —— 按 §2.1,任一 be_area 缺失返回 `null` +- `buildTierClassifier(): Promise<(ypt: number|null) => 'A'|'B'|'C'|null>` + - 查全库所有公共+自建材料的 5 个 `*_be_area` 列(只取这几列,轻量) + - 对每条算 Ypt,过滤掉 null,升序,按 §2.2 定出两个边界值 + - 返回分类器:`ypt==null → null`;否则按边界返回 A/B/C +- `annotate(materials, classifier): void` —— 给每条材料原地挂上 `ypt`、`health_tier` + +### 3.2 三个列表接口接入 + +均调用上述工具,逻辑不各自重写: + +1. `/api/mtrl`(材料库,`controllers/material.ts` 列表处理器) +2. `/api/proj/space/mtrl`(项目选材,`controllers/project.ts`) +3. `/api/proj/temp/:type/space/mtrl`(模板选材,`controllers/template.ts`) + +### 3.3 接口参数扩展 + +- `sort_key` 枚举增加 `ypt`;单污染物 Yp 排序复用已有的 `methanal_be_area` 等枚举值 +- 新增 `group_by_tier`:`0 | 1`(默认 0) +- 响应:每条材料新增 `ypt: number|null`、`health_tier: 'A'|'B'|'C'|null` + +### 3.4 排序语义 + +- `group_by_tier=0`:全库按 `sort_key` 升序;该排序值为空的材料排最后 +- `group_by_tier=1`:先按档 **A → B → C → (缺值 null)**,**档内**再按 `sort_key` 升序 +- **分页**照常在整个排好序的列表上切片(`page`/`size`);由前端在档位变化处插分区标题,无需一次拉全量 + +### 3.5 必要的小重构 + +现有 `/api/mtrl` 仅在"公共+自建都有结果"时才走内存排序+切片;只有一种类型时直接返回 Prisma 排序结果。因 `ypt`/`health_tier` 是计算值、Prisma 无法排序,需让 **"注解 → 排序 → 切片"在所有情况统一走内存路径**。改动限定在列表处理器内部,不影响其它逻辑。 + +## 4. 前端设计 + +用户端(iapip-web)与管理端(iapip-ms)是两个独立 app,前端代码各写一份;后端已扛计算重活,前端仅负责控件与展示。 + +### 4.1 两个控件(材料列表顶部) + +- **「排序依据」下拉**:综合 Ypt(默认)/ 甲醛 Yp / TVOC Yp / 苯 Yp / 甲苯 Yp / 二甲苯 Yp + - 映射 `sort_key ∈ {ypt, methanal_be_area, tvoc_be_area, benzene_be_area, toluene_be_area, p_xylene_be_area}`,恒 `sort_val=asc` +- **「按健康等级分组」开关** → `group_by_tier` 0/1 + +### 4.2 展示 + +- 每个材料卡片加**健康档标签**:A=绿 / B=黄 / C=红 / 缺值=灰"—"。不分组时也显示 +- 分组开时:渲染 **A / B / C /「数据不全」** 分区标题,档内按所选依据升序 +- **默认视图**:综合 Ypt 升序、不分组、带档标签 + +### 4.3 落地范围 + +- 材料库(用户端 + 管理端):完整两控件 + 分组 + 标签 +- 项目 / 模板选材:档标签 + 排序依据下拉(分组开关可选,视界面空间) + +## 5. 边界情况 + +- 完整数据材料 < 3 个:按 `⌈n/3⌉`、`⌈2n/3⌉` 切,档位可能缺字母(正常,不报错) +- 相同 Ypt:落同一档,不强拆 +- 缺值材料:标签"—",分组时进「数据不全」区,恒排最后 +- 全库无任何完整材料:分类器对所有输入返回 null(全部"数据不全") +- 性能:`buildTierClassifier` 每次请求多一次只取 5 列的全库查询,材料量级上千条,可忽略 + +## 6. 测试 + +- `computeYpt`、`buildTierClassifier` 为纯函数,加单元测试覆盖: + - Ypt 公式数值正确性 + - 任一 be_area 缺失 → null + - 三分位边界(n 可整除 / 不可整除)、并列同档、小 n(1/2 个) +- 后端当前无测试框架,随本功能引入轻量框架(vitest),仅测这两个纯函数 +- 排序/分组的接口行为通过手动验证(三接口 + 两控件组合) + +## 7. 非目标(YAGNI) + +- 不做降序 / 自定义权重配置(权重按公式固定) +- 不迁移或清理历史 `health_level` 手动数据(仅停用于分组) +- 不做档位持久化 / 缓存(实时算,量级不大)