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 authz from '../middlewares/authz'
|
||||||
import Service from '../common/service'
|
import Service from '../common/service'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
import { annotate, buildTierClassifier, computeYpt, Tier } from '../common/material-ranking'
|
||||||
|
|
||||||
// const PREDICT_FIELD: ((keyof PublicMaterial) | (keyof SelfMaterial))[] = [
|
// const PREDICT_FIELD: ((keyof PublicMaterial) | (keyof SelfMaterial))[] = [
|
||||||
// 'methanal', 'methanal_min_be', 'methanal_be_area', 'methanal_be_roc',
|
// 'methanal', 'methanal_min_be', 'methanal_be_area', 'methanal_be_roc',
|
||||||
|
|
@ -21,6 +22,47 @@ import moment from 'moment'
|
||||||
const materialRouter = new Router()
|
const materialRouter = new Router()
|
||||||
materialRouter.prefix('/api/mtrl')
|
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 条
|
// 环保建材展示(公开,免认证):E0 优先,不足补 E1,最多 8 条
|
||||||
materialRouter.get('/eco', async ctx => {
|
materialRouter.get('/eco', async ctx => {
|
||||||
const take = 8
|
const take = 8
|
||||||
|
|
@ -198,7 +240,7 @@ materialRouter.get(
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
sort_key: {
|
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: {
|
sort_val: {
|
||||||
enum: ['asc', 'desc']
|
enum: ['asc', 'desc']
|
||||||
|
|
@ -210,6 +252,10 @@ materialRouter.get(
|
||||||
size: {
|
size: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
minimum: 1
|
minimum: 1
|
||||||
|
},
|
||||||
|
group_by_tier: {
|
||||||
|
type: 'number',
|
||||||
|
enum: [0, 1]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
@ -217,16 +263,20 @@ materialRouter.get(
|
||||||
}, { coerceTypes: true }),
|
}, { coerceTypes: true }),
|
||||||
async ctx => {
|
async ctx => {
|
||||||
const acctCert = ctx.state.acct_cert as AcctCert
|
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 materials: AnyObj[] = []
|
||||||
// 查询公共材料
|
// 查询公共材料
|
||||||
let pubMtrlQty = 0
|
|
||||||
if (!type || type == DATA_TYPE.PUBLIC) {
|
if (!type || type == DATA_TYPE.PUBLIC) {
|
||||||
const conditions: AnyObj = dynSearch(query, {
|
const conditions: AnyObj = dynSearch(query, {
|
||||||
fuzzyFields: ['material_id', 'name', 'category', 'brand', 'factory', 'spec', 'creator_name']
|
fuzzyFields: ['material_id', 'name', 'category', 'brand', 'factory', 'spec', 'creator_name']
|
||||||
})
|
})
|
||||||
|
delete conditions.take; delete conditions.skip
|
||||||
if (!query.sort_key) {
|
if (!query.sort_key) {
|
||||||
conditions.orderBy = [{ sort_order: 'asc' }, { updated_at: 'desc' }]
|
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 (!isNone(collected) && !acctCert.is_admin) {
|
||||||
if (!conditions.where) {
|
if (!conditions.where) {
|
||||||
|
|
@ -243,11 +293,9 @@ materialRouter.get(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const pubMtrls = await db.publicMaterial.findMany(conditions as never)
|
const pubMtrls = await db.publicMaterial.findMany(conditions as never)
|
||||||
pubMtrlQty = pubMtrls.length
|
|
||||||
materials.push(...pubMtrls)
|
materials.push(...pubMtrls)
|
||||||
}
|
}
|
||||||
// 查询自建材料
|
// 查询自建材料
|
||||||
let selfMtrlQty = 0
|
|
||||||
if (!type || type == DATA_TYPE.PRIVATE) {
|
if (!type || type == DATA_TYPE.PRIVATE) {
|
||||||
delete query.display
|
delete query.display
|
||||||
delete query.from
|
delete query.from
|
||||||
|
|
@ -257,40 +305,19 @@ materialRouter.get(
|
||||||
const selfConditions: AnyObj = dynSearch(query, {
|
const selfConditions: AnyObj = dynSearch(query, {
|
||||||
fuzzyFields: ['material_id', 'name', 'category', 'brand', 'factory', 'spec', 'creator_name']
|
fuzzyFields: ['material_id', 'name', 'category', 'brand', 'factory', 'spec', 'creator_name']
|
||||||
})
|
})
|
||||||
|
delete selfConditions.take; delete selfConditions.skip
|
||||||
if (!query.sort_key) {
|
if (!query.sort_key) {
|
||||||
selfConditions.orderBy = [{ sort_order: 'asc' }, { updated_at: 'desc' }]
|
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)
|
const selfMtrls = await db.selfMaterial.findMany(selfConditions as never)
|
||||||
selfMtrlQty = selfMtrls.length
|
|
||||||
materials.push(...selfMtrls)
|
materials.push(...selfMtrls)
|
||||||
}
|
}
|
||||||
// 排序和分页
|
// 统一:注解 ypt/health_tier + 排序(含 ypt/分组) + 分页
|
||||||
if (pubMtrlQty && selfMtrlQty) {
|
const classify = await loadTierClassifier()
|
||||||
const sk = query.sort_key
|
ctx.body = rankAndPaginate(materials, { ...query, group_by_tier }, classify)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue