feat(material): /api/mtrl 注解 ypt/health_tier 并支持 ypt 排序与分组
This commit is contained in:
parent
7027a7748f
commit
f5ec58031a
|
|
@ -9,6 +9,7 @@ import { ID_PREFIX, DATA_TYPE, ERR_CODE } from '../common/constants'
|
|||
import authz from '../middlewares/authz'
|
||||
import Service from '../common/service'
|
||||
import moment from 'moment'
|
||||
import { annotate, buildTierClassifier, computeYpt, Tier } from '../common/material-ranking'
|
||||
|
||||
// const PREDICT_FIELD: ((keyof PublicMaterial) | (keyof SelfMaterial))[] = [
|
||||
// 'methanal', 'methanal_min_be', 'methanal_be_area', 'methanal_be_roc',
|
||||
|
|
@ -21,6 +22,47 @@ import moment from 'moment'
|
|||
const materialRouter = new Router()
|
||||
materialRouter.prefix('/api/mtrl')
|
||||
|
||||
const BE_AREA_SELECT = {
|
||||
methanal_be_area: true, tvoc_be_area: true, benzene_be_area: true, toluene_be_area: true, p_xylene_be_area: true
|
||||
} as const
|
||||
|
||||
// 查全库(公共+自建)be_area,建三分位分类器
|
||||
async function loadTierClassifier(): Promise<(ypt: number | null) => Tier | null> {
|
||||
const [pub, self] = await Promise.all([
|
||||
db.publicMaterial.findMany({ select: BE_AREA_SELECT }),
|
||||
db.selfMaterial.findMany({ select: BE_AREA_SELECT })
|
||||
])
|
||||
return buildTierClassifier([...pub, ...self])
|
||||
}
|
||||
|
||||
// 注解 + 排序(ypt / *_be_area, 恒升序; group_by_tier 时先按档) + 分页
|
||||
const TIER_ORDER: Record<string, number> = { A: 0, B: 1, C: 2 }
|
||||
function rankAndPaginate(materials: AnyObj[], query: AnyObj, classify: (ypt: number | null) => Tier | null): AnyObj[] {
|
||||
let list: AnyObj[] = annotate(materials as never, classify)
|
||||
const sk = query.sort_key as string | undefined
|
||||
const group = query.group_by_tier == 1 || query.group_by_tier === true
|
||||
if (sk || group) {
|
||||
const tierRank = (t: Tier | null) => (t === null ? 3 : TIER_ORDER[t])
|
||||
list.sort((a, b) => {
|
||||
if (group) {
|
||||
const d = tierRank(a.health_tier) - tierRank(b.health_tier)
|
||||
if (d !== 0) return d
|
||||
}
|
||||
const k = sk ?? 'ypt'
|
||||
const av = a[k], bv = b[k]
|
||||
// 升序;null 排最后
|
||||
if (av === null || av === undefined) return bv === null || bv === undefined ? 0 : 1
|
||||
if (bv === null || bv === undefined) return -1
|
||||
return av === bv ? 0 : av > bv ? 1 : -1
|
||||
})
|
||||
}
|
||||
if (query.size) {
|
||||
const page = query.page ?? 1
|
||||
list = list.slice((page - 1) * query.size, page * query.size)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// 环保建材展示(公开,免认证):E0 优先,不足补 E1,最多 8 条
|
||||
materialRouter.get('/eco', async ctx => {
|
||||
const take = 8
|
||||
|
|
@ -198,7 +240,7 @@ materialRouter.get(
|
|||
]
|
||||
},
|
||||
sort_key: {
|
||||
enum: ['name', 'category', 'brand', 'factory', 'spec', 'eco_level', 'health_level', 'sort_order', 'methanal_be_area', 'tvoc_be_area', 'benzene_be_area', 'toluene_be_area', 'p_xylene_be_area', 'creator_name', 'created_at', 'updated_at']
|
||||
enum: ['name', 'category', 'brand', 'factory', 'spec', 'eco_level', 'health_level', 'sort_order', 'methanal_be_area', 'tvoc_be_area', 'benzene_be_area', 'toluene_be_area', 'p_xylene_be_area', 'creator_name', 'created_at', 'updated_at', 'ypt']
|
||||
},
|
||||
sort_val: {
|
||||
enum: ['asc', 'desc']
|
||||
|
|
@ -210,6 +252,10 @@ materialRouter.get(
|
|||
size: {
|
||||
type: 'number',
|
||||
minimum: 1
|
||||
},
|
||||
group_by_tier: {
|
||||
type: 'number',
|
||||
enum: [0, 1]
|
||||
}
|
||||
},
|
||||
additionalProperties: false
|
||||
|
|
@ -217,16 +263,20 @@ materialRouter.get(
|
|||
}, { coerceTypes: true }),
|
||||
async ctx => {
|
||||
const acctCert = ctx.state.acct_cert as AcctCert
|
||||
const { type, collected, ...query } = ctx.state.query
|
||||
// group_by_tier 不是 Prisma 列,需从传给 dynSearch 的 query 中剔除,否则 findMany 报未知字段
|
||||
const { type, collected, group_by_tier, ...query } = ctx.state.query
|
||||
let materials: AnyObj[] = []
|
||||
// 查询公共材料
|
||||
let pubMtrlQty = 0
|
||||
if (!type || type == DATA_TYPE.PUBLIC) {
|
||||
const conditions: AnyObj = dynSearch(query, {
|
||||
fuzzyFields: ['material_id', 'name', 'category', 'brand', 'factory', 'spec', 'creator_name']
|
||||
})
|
||||
delete conditions.take; delete conditions.skip
|
||||
if (!query.sort_key) {
|
||||
conditions.orderBy = [{ sort_order: 'asc' }, { updated_at: 'desc' }]
|
||||
} else if (query.sort_key === 'ypt') {
|
||||
// ypt 是内存计算字段,非 Prisma 列,不能下推为 orderBy
|
||||
delete conditions.orderBy
|
||||
}
|
||||
if (!isNone(collected) && !acctCert.is_admin) {
|
||||
if (!conditions.where) {
|
||||
|
|
@ -243,11 +293,9 @@ materialRouter.get(
|
|||
}
|
||||
}
|
||||
const pubMtrls = await db.publicMaterial.findMany(conditions as never)
|
||||
pubMtrlQty = pubMtrls.length
|
||||
materials.push(...pubMtrls)
|
||||
}
|
||||
// 查询自建材料
|
||||
let selfMtrlQty = 0
|
||||
if (!type || type == DATA_TYPE.PRIVATE) {
|
||||
delete query.display
|
||||
delete query.from
|
||||
|
|
@ -257,40 +305,19 @@ materialRouter.get(
|
|||
const selfConditions: AnyObj = dynSearch(query, {
|
||||
fuzzyFields: ['material_id', 'name', 'category', 'brand', 'factory', 'spec', 'creator_name']
|
||||
})
|
||||
delete selfConditions.take; delete selfConditions.skip
|
||||
if (!query.sort_key) {
|
||||
selfConditions.orderBy = [{ sort_order: 'asc' }, { updated_at: 'desc' }]
|
||||
} else if (query.sort_key === 'ypt') {
|
||||
// ypt 是内存计算字段,非 Prisma 列,不能下推为 orderBy
|
||||
delete selfConditions.orderBy
|
||||
}
|
||||
const selfMtrls = await db.selfMaterial.findMany(selfConditions as never)
|
||||
selfMtrlQty = selfMtrls.length
|
||||
materials.push(...selfMtrls)
|
||||
}
|
||||
// 排序和分页
|
||||
if (pubMtrlQty && selfMtrlQty) {
|
||||
const sk = query.sort_key
|
||||
const sv = query.sort_val == 'asc' ? 0 : 1
|
||||
materials.sort((a, b) => {
|
||||
const keys = sk ? [sk] : ['sort_order', 'updated_at']
|
||||
for (const k of keys) {
|
||||
const av = !sv ? a[k] : b[k]
|
||||
const bv = sv ? a[k] : b[k]
|
||||
const dir = sk ? (sv ? -1 : 1) : (k === 'updated_at' ? -1 : 1)
|
||||
if (av && bv) {
|
||||
if (av != bv) return av > bv ? dir : -dir
|
||||
} else if (av && !bv) {
|
||||
return dir
|
||||
} else if (!av && bv) {
|
||||
return -dir
|
||||
}
|
||||
}
|
||||
return 0
|
||||
})
|
||||
if (query.size) {
|
||||
const page = query.page ?? 1
|
||||
const size = query.size
|
||||
materials = materials.slice((page - 1) * size, page * size)
|
||||
}
|
||||
}
|
||||
ctx.body = materials
|
||||
// 统一:注解 ypt/health_tier + 排序(含 ypt/分组) + 分页
|
||||
const classify = await loadTierClassifier()
|
||||
ctx.body = rankAndPaginate(materials, { ...query, group_by_tier }, classify)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue