import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { type StandardCode } from '@airpredict/shared'; import { PrismaService } from '../prisma/prisma.service'; import { PredictionService } from '../prediction/prediction.service'; import { CreateSpaceDto, UpdateSpaceDto, PrecalcDto } from './dto/space.dto'; @Injectable() export class SpacesService { constructor( private prisma: PrismaService, private prediction: PredictionService, ) {} /** 预计算:不落库,直接返回浓度+贡献率 */ async precalc(dto: PrecalcDto) { return this.prediction.computeSpace({ volume: dto.volume, temperature: dto.temperature, humidity: dto.humidity, ventilationRate: dto.ventilationRate, standard: dto.standard as StandardCode, materials: dto.materials.map((m) => ({ materialId: m.materialId, usageAmount: m.usageAmount })), }); } async create(orgId: string, dto: CreateSpaceDto) { await this.assertProjectOwned(orgId, dto.projectId); const space = await this.prisma.space.create({ data: { id: this.genId('S'), projectId: dto.projectId, name: dto.name, type: dto.type, layout: dto.layout ?? 'uniform', height: dto.height, area: dto.area, volume: dto.volume, temperature: dto.temperature, humidity: dto.humidity, ventilationRate: dto.ventilationRate, standard: dto.standard, materials: { create: dto.materials.map((m) => ({ materialId: m.materialId, usageUnit: m.usageUnit ?? 'm²', usageAmount: m.usageAmount, })), }, }, include: { materials: { include: { material: true } } }, }); await this.touchProject(dto.projectId); return space; } async update(orgId: string, id: string, dto: UpdateSpaceDto) { const space = await this.prisma.space.findUnique({ where: { id }, include: { project: true } }); if (!space) throw new NotFoundException('空间不存在'); if (space.project.ownerOrgId !== orgId) throw new ForbiddenException('无权操作'); const { materials, ...rest } = dto; const updated = await this.prisma.space.update({ where: { id }, data: { ...rest, // 配置已改,predictedConc 失效 predictedConc: Prisma.JsonNull, ...(materials ? { materials: { deleteMany: {}, create: materials.map((m) => ({ materialId: m.materialId, usageUnit: m.usageUnit ?? 'm²', usageAmount: m.usageAmount, })), }, } : {}), }, include: { materials: { include: { material: true } } }, }); await this.touchProject(space.projectId); return updated; } async remove(orgId: string, id: string) { const space = await this.prisma.space.findUnique({ where: { id }, include: { project: true } }); if (!space) throw new NotFoundException('空间不存在'); if (space.project.ownerOrgId !== orgId) throw new ForbiddenException('无权操作'); await this.prisma.space.delete({ where: { id } }); await this.touchProject(space.projectId); return { success: true }; } private async assertProjectOwned(orgId: string, projectId: string) { const p = await this.prisma.project.findUnique({ where: { id: projectId } }); if (!p) throw new NotFoundException('项目不存在'); if (p.ownerOrgId !== orgId) throw new ForbiddenException('无权操作'); } /** 配置变更后,项目回到未生成报告状态 */ private async touchProject(projectId: string) { await this.prisma.project.update({ where: { id: projectId }, data: { status: 'configuring', rating: null, reportGeneratedAt: null }, }); } private genId(prefix: string): string { let s = ''; const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; for (let i = 0; i < 8; i++) s += chars[Math.floor(Math.random() * chars.length)]; return prefix + s; } }