docs: 材料健康分级与 Yp 排序设计文档
- Ypt 综合值公式 + 全库三分位定 A/B/C 健康档 - 缺值材料不分档、排最后 - 后端共享 material-ranking 工具接入三个列表接口 - 前端两控件(排序依据+分组开关)+ 档标签 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCShKevM5prWZhp1kk1iGh
This commit is contained in:
parent
d6c5a6b497
commit
021d09f33b
|
|
@ -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` 手动数据(仅停用于分组)
|
||||
- 不做档位持久化 / 缓存(实时算,量级不大)
|
||||
Loading…
Reference in New Issue