diff --git a/空气质量预测/源码/服务端/iapip-svr/src/controllers/material.ts b/空气质量预测/源码/服务端/iapip-svr/src/controllers/material.ts new file mode 100644 index 0000000..9a059e5 --- /dev/null +++ b/空气质量预测/源码/服务端/iapip-svr/src/controllers/material.ts @@ -0,0 +1,1839 @@ +import Router from 'koa-router' +import authn from '../middlewares/authn' +import { ADMIN_TYPE, AcctCert, AdminCert, AnyObj, CERT_TYPE, UserCert } from '../common/types' +import validate from '../middlewares/validator' +import { db } from '..' +import { dynSearch, fail, isNone } from '../common/utils' +import idKit from '../common/id-kit' +import { ID_PREFIX, DATA_TYPE, ERR_CODE } from '../common/constants' +import authz from '../middlewares/authz' +import Service from '../common/service' +import moment from 'moment' + +// const PREDICT_FIELD: ((keyof PublicMaterial) | (keyof SelfMaterial))[] = [ +// 'methanal', 'methanal_min_be', 'methanal_be_area', 'methanal_be_roc', +// 'tvoc', 'tvoc_min_be', 'tvoc_be_area', 'tvoc_be_roc', +// 'benzene', 'benzene_min_be', 'benzene_be_area', 'benzene_be_roc', +// 'toluene', 'toluene_min_be', 'toluene_be_area', 'toluene_be_roc', +// 'p_xylene', 'p_xylene_min_be', 'p_xylene_be_area', 'p_xylene_be_roc' +// ] + +const materialRouter = new Router() +materialRouter.prefix('/api/mtrl') + +// 环保建材展示(公开,免认证):E0 优先,不足补 E1,最多 8 条 +materialRouter.get('/eco', async ctx => { + const take = 8 + const selectFields = { + material_id: true, name: true, category: true, + brand: true, factory: true, eco_level: true, methanal: true + } + const e0 = await db.publicMaterial.findMany({ + where: { display: 1, eco_level: 'E0' }, + orderBy: { material_id: 'desc' }, + take, + select: selectFields + }) + let list = e0 + if (list.length < take) { + const e1 = await db.publicMaterial.findMany({ + where: { display: 1, eco_level: 'E1' }, + orderBy: { material_id: 'desc' }, + take: take - list.length, + select: selectFields + }) + list = list.concat(e1) + } + ctx.body = list +}) + +// 查询材料 +materialRouter.get( + '/', + authn(CERT_TYPE.ACCOUNT), + validate({ + query: { + type: 'object', + properties: { + material_id: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + name: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + type: { + enum: [DATA_TYPE.PUBLIC, DATA_TYPE.PRIVATE] + }, + category: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + brand: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + factory: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + spec: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + eco_level: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + display: { + enum: [0, 1] + }, + collected: { + enum: [0, 1] + }, + from: { + type: 'string' + }, + creator_id: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + creator_name: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + health_level: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + 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'] + }, + sort_val: { + enum: ['asc', 'desc'] + }, + page: { + type: 'number', + minimum: 1 + }, + size: { + type: 'number', + minimum: 1 + } + }, + additionalProperties: false + } + }, { coerceTypes: true }), + async ctx => { + const acctCert = ctx.state.acct_cert as AcctCert + const { type, collected, ...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'] + }) + if (!query.sort_key) { + conditions.orderBy = [{ sort_order: 'asc' }, { updated_at: 'desc' }] + } + if (!isNone(collected) && !acctCert.is_admin) { + if (!conditions.where) { + conditions.where = {} + } + conditions.where.collectors = collected ? { + some: { + user_id: acctCert.id + } + } : { + none: { + user_id: acctCert.id + } + } + } + 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 + if (!acctCert.is_admin) { + query.creator_id = acctCert.id + } + const selfConditions: AnyObj = dynSearch(query, { + fuzzyFields: ['material_id', 'name', 'category', 'brand', 'factory', 'spec', 'creator_name'] + }) + if (!query.sort_key) { + selfConditions.orderBy = [{ sort_order: 'asc' }, { updated_at: 'desc' }] + } + 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 + } +) + +/** + * 公共材料 + */ + +// 批量新建公共材料 +materialRouter.post( + '/pub/batch', + authn(CERT_TYPE.ADMIN, true), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN), + validate({ + body: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string' + }, + category: { + type: 'string' + }, + brand: { + type: 'string' + }, + factory: { + type: 'string' + }, + spec: { + type: 'string' + }, + eco_level: { + type: 'string' + }, + health_level: { + type: 'string' + }, + display: { + enum: [0, 1] + }, + methanal: { + type: 'number', + minimum: 0 + }, + methanal_min_be: { + type: 'number', + minimum: 0 + }, + methanal_be_area: { + type: 'number', + minimum: 0 + }, + methanal_be_roc: { + type: 'number', + minimum: 0 + }, + tvoc: { + type: 'number', + minimum: 0 + }, + tvoc_min_be: { + type: 'number', + minimum: 0 + }, + tvoc_be_area: { + type: 'number', + minimum: 0 + }, + tvoc_be_roc: { + type: 'number', + minimum: 0 + }, + benzene: { + type: 'number', + minimum: 0 + }, + benzene_min_be: { + type: 'number', + minimum: 0 + }, + benzene_be_area: { + type: 'number', + minimum: 0 + }, + benzene_be_roc: { + type: 'number', + minimum: 0 + }, + toluene: { + type: 'number', + minimum: 0 + }, + toluene_min_be: { + type: 'number', + minimum: 0 + }, + toluene_be_area: { + type: 'number', + minimum: 0 + }, + toluene_be_roc: { + type: 'number', + minimum: 0 + }, + p_xylene: { + type: 'number', + minimum: 0 + }, + p_xylene_min_be: { + type: 'number', + minimum: 0 + }, + p_xylene_be_area: { + type: 'number', + minimum: 0 + }, + p_xylene_be_roc: { + type: 'number', + minimum: 0 + }, + from: { + type: 'string' + } + }, + additionalProperties: false + }, + minItems: 1 + } + }), + async ctx => { + const adminCert = ctx.state.acct_cert as AdminCert + ctx.body = await Service.createPublicMaterials(adminCert, ctx.request.body) + } +) + +// 新建公共材料 +materialRouter.post( + '/pub', + authn(CERT_TYPE.ADMIN, true), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN), + validate({ + body: { + type: 'object', + properties: { + name: { + type: 'string' + }, + category: { + type: 'string' + }, + brand: { + type: 'string' + }, + factory: { + type: 'string' + }, + spec: { + type: 'string' + }, + eco_level: { + type: 'string' + }, + health_level: { + type: 'string' + }, + display: { + enum: [0, 1] + }, + methanal: { + type: 'number', + minimum: 0 + }, + methanal_min_be: { + type: 'number', + minimum: 0 + }, + methanal_be_area: { + type: 'number', + minimum: 0 + }, + methanal_be_roc: { + type: 'number', + minimum: 0 + }, + tvoc: { + type: 'number', + minimum: 0 + }, + tvoc_min_be: { + type: 'number', + minimum: 0 + }, + tvoc_be_area: { + type: 'number', + minimum: 0 + }, + tvoc_be_roc: { + type: 'number', + minimum: 0 + }, + benzene: { + type: 'number', + minimum: 0 + }, + benzene_min_be: { + type: 'number', + minimum: 0 + }, + benzene_be_area: { + type: 'number', + minimum: 0 + }, + benzene_be_roc: { + type: 'number', + minimum: 0 + }, + toluene: { + type: 'number', + minimum: 0 + }, + toluene_min_be: { + type: 'number', + minimum: 0 + }, + toluene_be_area: { + type: 'number', + minimum: 0 + }, + toluene_be_roc: { + type: 'number', + minimum: 0 + }, + p_xylene: { + type: 'number', + minimum: 0 + }, + p_xylene_min_be: { + type: 'number', + minimum: 0 + }, + p_xylene_be_area: { + type: 'number', + minimum: 0 + }, + p_xylene_be_roc: { + type: 'number', + minimum: 0 + }, + from: { + type: 'string' + } + }, + additionalProperties: false + } + }), + async ctx => { + const adminCert = ctx.state.acct_cert as AdminCert + ctx.body = await Service.createPublicMaterial(adminCert, ctx.request.body) + } +) + +// 收录自建材料 +materialRouter.post( + '/pub/self', + authn(CERT_TYPE.ADMIN, true), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN), + validate({ + body: { + type: 'object', + properties: { + material_id: { + type: 'string' + }, + creator_id: { + type: 'string' + }, + display: { + enum: [0, 1] + } + }, + required: ['material_id', 'creator_id'], + additionalProperties: false + } + }), + async ctx => { + const selfMtrl = await db.selfMaterial.findFirst({ + where: { + material_id: ctx.request.body.material_id, + creator_id: ctx.request.body.creator_id + } + }) + if (!selfMtrl) { + ctx.throw(422, fail(ERR_CODE.MTRL_NOT_FOUNT, '材料不存在')) + return + } + ctx.body = await Service.collectSelfMaterial(selfMtrl) + } +) + +// 修改公共材料 +materialRouter.patch( + '/pub', + authn(CERT_TYPE.ADMIN, true), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN || (cert.type == ADMIN_TYPE.ADMIN && cert.authz_mtrl_mgmt == 1)), + validate({ + query: { + type: 'object', + properties: { + material_id: { + type: 'string' + } + }, + required: ['material_id'], + additionalProperties: false + }, + body: { + type: 'object', + properties: { + name: { + type: 'string' + }, + category: { + type: 'string' + }, + brand: { + type: 'string' + }, + factory: { + type: 'string' + }, + spec: { + type: 'string' + }, + eco_level: { + type: 'string' + }, + health_level: { + type: 'string' + }, + display: { + enum: [0, 1] + }, + methanal: { + type: 'number', + minimum: 0 + }, + methanal_min_be: { + type: 'number', + minimum: 0 + }, + methanal_be_area: { + type: 'number', + minimum: 0 + }, + methanal_be_roc: { + type: 'number', + minimum: 0 + }, + tvoc: { + type: 'number', + minimum: 0 + }, + tvoc_min_be: { + type: 'number', + minimum: 0 + }, + tvoc_be_area: { + type: 'number', + minimum: 0 + }, + tvoc_be_roc: { + type: 'number', + minimum: 0 + }, + benzene: { + type: 'number', + minimum: 0 + }, + benzene_min_be: { + type: 'number', + minimum: 0 + }, + benzene_be_area: { + type: 'number', + minimum: 0 + }, + benzene_be_roc: { + type: 'number', + minimum: 0 + }, + toluene: { + type: 'number', + minimum: 0 + }, + toluene_min_be: { + type: 'number', + minimum: 0 + }, + toluene_be_area: { + type: 'number', + minimum: 0 + }, + toluene_be_roc: { + type: 'number', + minimum: 0 + }, + p_xylene: { + type: 'number', + minimum: 0 + }, + p_xylene_min_be: { + type: 'number', + minimum: 0 + }, + p_xylene_be_area: { + type: 'number', + minimum: 0 + }, + p_xylene_be_roc: { + type: 'number', + minimum: 0 + } + }, + additionalProperties: false + } + }), + async ctx => { + ctx.body = await Service.updatePublicMaterial(ctx.state.query.material_id, ctx.request.body) + } +) + +// 调整公共材料排序(厂商竞价排名预留) +materialRouter.patch( + '/pub/sort', + authn(CERT_TYPE.ADMIN, true), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN), + validate({ + body: { + type: 'object', + properties: { + material_id: { + type: 'string' + }, + sort_order: { + type: 'number', + minimum: 0 + } + }, + required: ['material_id', 'sort_order'], + additionalProperties: false + } + }), + async ctx => { + const { material_id, sort_order } = ctx.request.body + const exists = await db.publicMaterial.findFirst({ where: { material_id }, select: { material_id: true } }) + if (!exists) { + ctx.throw(422, fail(ERR_CODE.MTRL_NOT_FOUNT, '材料不存在')) + return + } + await db.publicMaterial.update({ + where: { material_id }, + data: { sort_order } + }) + ctx.body = { material_id, sort_order } + } +) + +// 批量删除公共材料 +materialRouter.delete( + '/pub', + authn(CERT_TYPE.ADMIN, true), + authz(cert => cert.type == ADMIN_TYPE.SUPER_ADMIN), + validate({ + query: { + type: 'object', + properties: { + material_id: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + }, + minItems: 1 + } + ] + } + }, + required: ['material_id'], + additionalProperties: false + } + }), + async ctx => { + ctx.body = await Service.deletePublicMaterial(ctx.state.query.material_id) + } +) + +// 收藏/取消收藏公共材料 +materialRouter.post( + '/pub/collect', + authn(CERT_TYPE.USER), + validate({ + body: { + type: 'object', + properties: { + material_id: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + }, + minItems: 1 + } + ] + }, + c: { + enum: [0, 1] + } + }, + required: ['material_id', 'c'], + additionalProperties: false + } + }), + async ctx => { + const userCert = ctx.state.acct_cert as UserCert + const ids = Array.isArray(ctx.request.body.material_id) ? ctx.request.body.material_id as string[] : [ctx.request.body.material_id as string] + if (ctx.request.body.c) { + const collectedIds = (await db.publicMaterialCollection.findMany({ + select: { + material_id: true + }, + where: { + user_id: userCert.id, + material_id: { + in: ids + } + } + })).map(mtrl => mtrl.material_id) + const collectableIds = (await db.publicMaterial.findMany({ + select: { + material_id: true + }, + where: { + material_id: { + in: ids, + notIn: collectedIds.length > 0 ? collectedIds : undefined + } + } + })).map(mtrl => mtrl.material_id) + ctx.body = collectableIds.length > 0 ? await db.publicMaterialCollection.createMany({ + data: collectableIds.map(id => ({ + user_id: userCert.id, + material_id: id + })) + }) : { count: 0 } + } else { + ctx.body = await db.publicMaterialCollection.deleteMany({ + where: { + user_id: userCert.id, + material_id: { + in: ids + } + } + }) + } + } +) + +// 查询公共材料 +materialRouter.get( + '/pub', + authn(CERT_TYPE.ACCOUNT), + validate({ + query: { + type: 'object', + properties: { + material_id: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + name: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + category: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + brand: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + factory: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + spec: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + eco_level: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + display: { + enum: [0, 1] + }, + collected: { + enum: [0, 1] + }, + from: { + type: 'string' + }, + creator_id: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + creator_name: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + with_cltrs: { + enum: [0, 1] + }, + health_level: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + sort_key: { + anyOf: [ + { + 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', 'created_at', 'updated_at'] + }, + { + type: 'array', + items: { + 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', 'created_at', 'updated_at'] + } + } + ] + }, + sort_val: { + anyOf: [ + { + enum: ['asc', 'desc'] + }, + { + type: 'array', + items: { + enum: ['asc', 'desc'] + } + } + ] + }, + page: { + type: 'number', + minimum: 1 + }, + size: { + type: 'number', + minimum: 1 + } + }, + additionalProperties: false + } + }, { coerceTypes: true }), + async ctx => { + const acctCert = ctx.state.acct_cert as AcctCert + const { collected, with_cltrs, ...query } = ctx.state.query + const conditions: AnyObj = { + ...dynSearch(query, { + fuzzyFields: ['material_id', 'name', 'category', 'brand', 'factory', 'spec', 'creator_name'], + relCreator: { type: 2, field: 'username' } + }), + include: { + collectors: with_cltrs ? { + select: { + user_id: true + } + } : false, + related_user: { + select: { + name: true, + username: true + } + }, + related_admin: { + select: { + name: true, + username: true + } + } + } + } + if (!query.sort_key) { + conditions.orderBy = [{ sort_order: 'asc' }, { updated_at: 'desc' }] + } + if (!isNone(collected) && !acctCert.is_admin) { + if (!conditions.where) { + conditions.where = {} + } + conditions.where.collectors = collected ? { + some: { + user_id: acctCert.id + } + } : { + none: { + user_id: acctCert.id + } + } + } + const materials = await db.publicMaterial.findMany(conditions as never) + if (with_cltrs && !acctCert.is_admin) { + materials.forEach((mtrl: AnyObj) => { + mtrl.collected = mtrl.collectors.find((cltr: AnyObj) => cltr.user_id == acctCert.id) ? 1 : 0 + mtrl.collectors = [] + }) + } + ctx.body = materials + } +) + +/** + * 自建材料 + */ + +// 批量新建自建材料 +materialRouter.post( + '/self/batch', + authn(CERT_TYPE.USER, true), + validate({ + body: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string' + }, + category: { + type: 'string' + }, + brand: { + type: 'string' + }, + factory: { + type: 'string' + }, + spec: { + type: 'string' + }, + eco_level: { + type: 'string' + }, + methanal: { + type: 'number', + minimum: 0 + }, + methanal_min_be: { + type: 'number', + minimum: 0 + }, + methanal_be_area: { + type: 'number', + minimum: 0 + }, + methanal_be_roc: { + type: 'number', + minimum: 0 + }, + tvoc: { + type: 'number', + minimum: 0 + }, + tvoc_min_be: { + type: 'number', + minimum: 0 + }, + tvoc_be_area: { + type: 'number', + minimum: 0 + }, + tvoc_be_roc: { + type: 'number', + minimum: 0 + }, + benzene: { + type: 'number', + minimum: 0 + }, + benzene_min_be: { + type: 'number', + minimum: 0 + }, + benzene_be_area: { + type: 'number', + minimum: 0 + }, + benzene_be_roc: { + type: 'number', + minimum: 0 + }, + toluene: { + type: 'number', + minimum: 0 + }, + toluene_min_be: { + type: 'number', + minimum: 0 + }, + toluene_be_area: { + type: 'number', + minimum: 0 + }, + toluene_be_roc: { + type: 'number', + minimum: 0 + }, + p_xylene: { + type: 'number', + minimum: 0 + }, + p_xylene_min_be: { + type: 'number', + minimum: 0 + }, + p_xylene_be_area: { + type: 'number', + minimum: 0 + }, + p_xylene_be_roc: { + type: 'number', + minimum: 0 + } + }, + additionalProperties: false + }, + minItems: 1 + } + }), + async ctx => { + const userCert = ctx.state.acct_cert as UserCert + const ids = idKit.yMdHmSBatch(ID_PREFIX.SELF_MATERIAL, ctx.request.body.length) + ctx.body = await db.selfMaterial.createMany({ + data: ctx.request.body.map((item: AnyObj, index: number) => { + const at = moment().add(-index, 's').toDate() + return { + material_id: ids[index], + ...item, + creator_id: userCert.id, + creator_name: userCert.name, + created_at: at, + updated_at: at + } + }) + }) + } +) + +// 新建自建材料 +materialRouter.post( + '/self', + authn(CERT_TYPE.USER, true), + validate({ + body: { + type: 'object', + properties: { + name: { + type: 'string' + }, + category: { + type: 'string' + }, + brand: { + type: 'string' + }, + factory: { + type: 'string' + }, + spec: { + type: 'string' + }, + eco_level: { + type: 'string' + }, + health_level: { + type: 'string' + }, + methanal: { + type: 'number', + minimum: 0 + }, + methanal_min_be: { + type: 'number', + minimum: 0 + }, + methanal_be_area: { + type: 'number', + minimum: 0 + }, + methanal_be_roc: { + type: 'number', + minimum: 0 + }, + tvoc: { + type: 'number', + minimum: 0 + }, + tvoc_min_be: { + type: 'number', + minimum: 0 + }, + tvoc_be_area: { + type: 'number', + minimum: 0 + }, + tvoc_be_roc: { + type: 'number', + minimum: 0 + }, + benzene: { + type: 'number', + minimum: 0 + }, + benzene_min_be: { + type: 'number', + minimum: 0 + }, + benzene_be_area: { + type: 'number', + minimum: 0 + }, + benzene_be_roc: { + type: 'number', + minimum: 0 + }, + toluene: { + type: 'number', + minimum: 0 + }, + toluene_min_be: { + type: 'number', + minimum: 0 + }, + toluene_be_area: { + type: 'number', + minimum: 0 + }, + toluene_be_roc: { + type: 'number', + minimum: 0 + }, + p_xylene: { + type: 'number', + minimum: 0 + }, + p_xylene_min_be: { + type: 'number', + minimum: 0 + }, + p_xylene_be_area: { + type: 'number', + minimum: 0 + }, + p_xylene_be_roc: { + type: 'number', + minimum: 0 + } + }, + additionalProperties: false + } + }), + async ctx => { + const userCert = ctx.state.acct_cert as UserCert + ctx.body = await db.selfMaterial.create({ + data: { + material_id: await idKit.yMdHmSrr(ID_PREFIX.SELF_MATERIAL, id => db.selfMaterial.findFirst({ where: { material_id: id, creator_id: userCert.id } })), + ...ctx.request.body, + creator_id: userCert.id, + creator_name: userCert.name + } + }) + } +) + +// 修改自建材料 +materialRouter.patch( + '/self', + authn(CERT_TYPE.USER), + validate({ + query: { + type: 'object', + properties: { + material_id: { + type: 'string' + } + }, + required: ['material_id'], + additionalProperties: false + }, + body: { + type: 'object', + properties: { + name: { + type: 'string' + }, + category: { + type: 'string' + }, + brand: { + type: 'string' + }, + factory: { + type: 'string' + }, + spec: { + type: 'string' + }, + eco_level: { + type: 'string' + }, + health_level: { + type: 'string' + }, + methanal: { + type: 'number', + minimum: 0 + }, + methanal_min_be: { + type: 'number', + minimum: 0 + }, + methanal_be_area: { + type: 'number', + minimum: 0 + }, + methanal_be_roc: { + type: 'number', + minimum: 0 + }, + tvoc: { + type: 'number', + minimum: 0 + }, + tvoc_min_be: { + type: 'number', + minimum: 0 + }, + tvoc_be_area: { + type: 'number', + minimum: 0 + }, + tvoc_be_roc: { + type: 'number', + minimum: 0 + }, + benzene: { + type: 'number', + minimum: 0 + }, + benzene_min_be: { + type: 'number', + minimum: 0 + }, + benzene_be_area: { + type: 'number', + minimum: 0 + }, + benzene_be_roc: { + type: 'number', + minimum: 0 + }, + toluene: { + type: 'number', + minimum: 0 + }, + toluene_min_be: { + type: 'number', + minimum: 0 + }, + toluene_be_area: { + type: 'number', + minimum: 0 + }, + toluene_be_roc: { + type: 'number', + minimum: 0 + }, + p_xylene: { + type: 'number', + minimum: 0 + }, + p_xylene_min_be: { + type: 'number', + minimum: 0 + }, + p_xylene_be_area: { + type: 'number', + minimum: 0 + }, + p_xylene_be_roc: { + type: 'number', + minimum: 0 + } + }, + additionalProperties: false + } + }), + async ctx => { + const userCert = ctx.state.acct_cert as UserCert + const updated = await db.selfMaterial.update({ + where: { + material_id_creator_id: { + material_id: ctx.state.query.material_id, + creator_id: userCert.id + } + }, + data: ctx.request.body + }) + // health_level 仅自建材料库(SelfMaterial)存储;项目材料快照(Material)与自建模板材料(SelfTemplateMaterial) + // 均无此列,同步时需剔除,否则 Prisma 会因未知字段报错 + const { health_level, ...syncBody } = ctx.request.body as AnyObj + const hasSync = Object.keys(syncBody).length > 0 + const projects = await db.project.findMany({ + where: { + creator_id: userCert.id, + status: 0, + spaces: { + some: { + materials: { + some: { + material_id: ctx.state.query.material_id + } + } + } + } + } + }) + if (hasSync && projects.length > 0) { + await db.material.updateMany({ + where: { + material_id: ctx.state.query.material_id, + creator_id: userCert.id, + project_id: { + in: projects.map(p => p.id) + } + }, + data: syncBody + }) + } + if (hasSync) { + await db.selfTemplateMaterial.updateMany({ + where: { + material_id: ctx.state.query.material_id, + creator_id: userCert.id + }, + data: syncBody + }) + } + ctx.body = updated + } +) + +// 批量删除自建材料 +materialRouter.delete( + '/self', + authn(CERT_TYPE.USER), + validate({ + query: { + type: 'object', + properties: { + material_id: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + }, + minItems: 1 + } + ] + } + }, + required: ['material_id'], + additionalProperties: false + } + }), + async ctx => { + const userCert = ctx.state.acct_cert as UserCert + const ids = Array.isArray(ctx.state.query.material_id) ? ctx.state.query.material_id as string[] : [ctx.state.query.material_id as string] + const deleted = await db.selfMaterial.deleteMany({ + where: { + material_id: { + in: ids + }, + creator_id: userCert.id + } + }) + const projects = await db.project.findMany({ + where: { + creator_id: userCert.id, + status: 0, + spaces: { + some: { + materials: { + some: { + material_id: { + in: ids + } + } + } + } + } + } + }) + if (projects.length > 0) { + await db.material.deleteMany({ + where: { + material_id: { + in: ids + }, + project_id: { + in: projects.map(p => p.id) + }, + creator_id: userCert.id + } + }) + } + await db.selfTemplateMaterial.deleteMany({ + where: { + material_id: { + in: ids + }, + creator_id: userCert.id + } + }) + ctx.body = deleted + } +) + +// 查询自建材料 +materialRouter.get( + '/self', + authn(CERT_TYPE.ACCOUNT), + validate({ + query: { + type: 'object', + properties: { + material_id: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + name: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + category: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + brand: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + factory: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + spec: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + eco_level: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + creator_id: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + creator_name: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + health_level: { + anyOf: [ + { + type: 'string' + }, + { + type: 'array', + items: { + type: 'string' + } + } + ] + }, + sort_key: { + anyOf: [ + { + enum: ['name', 'category', 'brand', 'factory', 'spec', 'eco_level', 'health_level', 'sort_order', 'creator_name', 'methanal_be_area', 'tvoc_be_area', 'benzene_be_area', 'toluene_be_area', 'p_xylene_be_area', 'created_at', 'updated_at'] + }, + { + type: 'array', + items: { + enum: ['name', 'category', 'brand', 'factory', 'spec', 'eco_level', 'health_level', 'sort_order', 'creator_name', 'methanal_be_area', 'tvoc_be_area', 'benzene_be_area', 'toluene_be_area', 'p_xylene_be_area', 'created_at', 'updated_at'] + } + } + ] + }, + sort_val: { + anyOf: [ + { + enum: ['asc', 'desc'] + }, + { + type: 'array', + items: { + enum: ['asc', 'desc'] + } + } + ] + }, + page: { + type: 'number', + minimum: 1 + }, + size: { + type: 'number', + minimum: 1 + } + }, + additionalProperties: false + } + }, { coerceTypes: true }), + async ctx => { + const acctCert = ctx.state.acct_cert as AcctCert + const query = ctx.state.query + if (!acctCert.is_admin) { + query.creator_id = acctCert.id + } + const conditions: AnyObj = { + ...dynSearch(query, { + fuzzyFields: ['material_id', 'name', 'category', 'brand', 'factory', 'spec', 'creator_name'], + relCreator: { type: 1, field: 'username' } + }), + include: { + creator: { + select: { + name: true, + username: true + } + } + } + } + if (!query.sort_key) { + conditions.orderBy = [{ sort_order: 'asc' }, { updated_at: 'desc' }] + } + ctx.body = await db.selfMaterial.findMany(conditions as never) + } +) + +export const materialRoutes = materialRouter.routes() \ No newline at end of file