airpredict/空气质量预测/源码/docs/superpowers/specs/2026-07-10-material-sorting...

6.3 KiB
Raw Blame History

材料健康分级与 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 = nullhealth_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 —— 给每条材料原地挂上 ypthealth_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_tier0 | 1(默认 0
  • 响应:每条材料新增 ypt: number|nullhealth_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. 测试

  • computeYptbuildTierClassifier 为纯函数,加单元测试覆盖:
    • Ypt 公式数值正确性
    • 任一 be_area 缺失 → null
    • 三分位边界n 可整除 / 不可整除)、并列同档、小 n1/2 个)
  • 后端当前无测试框架随本功能引入轻量框架vitest仅测这两个纯函数
  • 排序/分组的接口行为通过手动验证(三接口 + 两控件组合)

7. 非目标YAGNI

  • 不做降序 / 自定义权重配置(权重按公式固定)
  • 不迁移或清理历史 health_level 手动数据(仅停用于分组)
  • 不做档位持久化 / 缓存(实时算,量级不大)