import { Injectable, NotFoundException, BadRequestException, Optional } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository, IsNull, Not, In } from 'typeorm'; import { Room } from '../entities/room.entity'; import { Occupancy } from '../entities/occupancy.entity'; import { RoomExpense } from '../entities/room-expense.entity'; import { Bed } from '../entities/bed.entity'; import { Locker } from '../entities/locker.entity'; import { CreateRoomDto, UpdateRoomDto } from './dto/room.dto'; import { CreateBedDto, UpdateBedDto, BatchCreateBedDto } from './dto/bed.dto'; import { CreateLockerDto, UpdateLockerDto, BatchCreateLockerDto } from './dto/locker.dto'; import { RoomInspectionsService } from './room-inspections.service'; import { RoomQueryService } from './room-query.service'; import { RoomBedLockerService } from './room-bed-locker.service'; import { parseRoomNumber } from './room-number'; @Injectable() export class RoomsService { constructor( @InjectRepository(Room) private repo: Repository, @InjectRepository(Occupancy) private occRepo: Repository, @InjectRepository(RoomExpense) private roomExpRepo: Repository, @InjectRepository(Bed) private bedRepo: Repository, @InjectRepository(Locker) private lockerRepo: Repository, private dataSource: DataSource, private readonly inspectionsService: RoomInspectionsService, @Optional() private queryService?: RoomQueryService, @Optional() private beds?: RoomBedLockerService, ) {} private get queries(): RoomQueryService { if (!this.queryService) { this.queryService = new RoomQueryService(this.repo, this.occRepo, this.bedRepo, this.inspectionsService); } return this.queryService; } private get bedOps(): RoomBedLockerService { if (!this.beds) { this.beds = new RoomBedLockerService(this.repo, this.bedRepo, this.lockerRepo); } return this.beds; } /** * 智能解析房间号,自动推导楼栋、楼层、宿舍类型 * "4-102" → building:"4号楼", floor:1, roomType:"四人间" * "1-2-101" → building:"1-2栋", floor:1, roomType:"家庭房" * "3-301" → building:"3号楼", floor:3, roomType:"四人间" * "8-102" → building:"8号楼", floor:1, roomType:"爆改房" */ static parseRoomNumber(roomNumber: string) { return parseRoomNumber(roomNumber); } async findAll(query?: { building?: string; includeArchived?: boolean }) { const where: any = {}; if (query?.building) where.building = query.building; if (!query?.includeArchived) where.status = Not('archived'); return this.repo.find({ where, order: { roomNumber: 'ASC' } }); } async findOne(id: number) { const room = await this.repo.findOne({ where: { id } }); if (!room) throw new NotFoundException('宿舍不存在'); return room; } async findOneWithOccupants(id: number) { const room = await this.findOne(id); const occupants = await this.occRepo.find({ where: { roomId: id, checkOutDate: IsNull() }, relations: ['student'], order: { checkInDate: 'ASC' }, }); return { ...room, currentOccupants: occupants }; } async getRoomOverview(query?: { includeArchived?: boolean }) { const where: any = {}; if (!query?.includeArchived) where.status = Not('archived'); const rooms = await this.repo.find({ where, order: { building: 'ASC', roomNumber: 'ASC' } }); const result: any[] = []; for (const room of rooms) { const count = await this.occRepo.count({ where: { roomId: room.id, checkOutDate: IsNull() }, }); result.push({ ...room, currentCount: count }); } return result; } async create(dto: CreateRoomDto) { const parsed = RoomsService.parseRoomNumber(dto.roomNumber); const entity = this.repo.create({ ...dto, building: dto.building ?? parsed.building, floor: dto.floor ?? parsed.floor, roomType: dto.roomType ?? parsed.roomType, capacity: dto.capacity ?? parsed.capacity, }); const room = await this.repo.save(entity); await this.createDefaultBeds(room.id, room.capacity); return room; } async update(id: number, dto: UpdateRoomDto) { return this.dataSource.transaction(async (manager) => { const roomRepo = manager.getRepository(Room); const bedRepo = manager.getRepository(Bed); const occupancyRepo = manager.getRepository(Occupancy); const room = await roomRepo.findOne({ where: { id } }); if (!room) throw new NotFoundException('宿舍不存在'); if (dto.capacity !== undefined) { const [beds, activeOccupantCount] = await Promise.all([ bedRepo.find({ where: { roomId: id }, order: { bedNumber: 'ASC' } }), occupancyRepo.count({ where: { roomId: id, checkOutDate: IsNull() } }), ]); if (dto.capacity < activeOccupantCount) { throw new BadRequestException( `额定人数不能少于当前入住人数,当前有 ${activeOccupantCount} 人入住`, ); } if (dto.capacity < beds.length) { throw new BadRequestException( `额定人数不能少于现有床位数,当前有 ${beds.length} 张床位,请先删除多余的空闲床位`, ); } if (dto.capacity > beds.length) { const countToCreate = dto.capacity - beds.length; const start = this.getNextBedNumber(beds); const newBeds = Array.from({ length: countToCreate }, (_, index) => bedRepo.create({ roomId: id, bedNumber: `${start + index}号床` }), ); await bedRepo.save(newBeds); } if ( dto.status === undefined && room.status !== 'maintenance' && room.status !== 'archived' ) { dto = { ...dto, status: activeOccupantCount >= dto.capacity ? 'full' : 'available', }; } } await roomRepo.update(id, dto); return roomRepo.findOne({ where: { id } }); }); } async remove(id: number) { const room = await this.findOne(id); // 检查是否有在住人员 const activeCount = await this.occRepo.count({ where: { roomId: id, checkOutDate: IsNull() } }); if (activeCount > 0) throw new BadRequestException('该宿舍有在住人员,无法归档'); if (room.status === 'archived') throw new BadRequestException('该宿舍已归档'); // 软删除:归档而非物理删除 await this.repo.update(id, { status: 'archived' }); return { message: '已归档(数据已保留,可随时恢复)' }; } async batchRemove(ids: number[]) { if (!ids || ids.length === 0) throw new BadRequestException('请选择要归档的宿舍'); const rooms = await this.repo.find({ where: { id: In(ids) } }); const skipped: string[] = []; const targetIds: number[] = []; for (const r of rooms) { if (r.status === 'archived') { skipped.push(`${r.roomNumber}(已归档)`); continue; } const activeCount = await this.occRepo.count({ where: { roomId: r.id, checkOutDate: IsNull() }, }); if (activeCount > 0) { skipped.push(`${r.roomNumber}(有在住人员)`); continue; } targetIds.push(r.id); } let affected = 0; if (targetIds.length > 0) { const result = await this.repo .createQueryBuilder() .update() .set({ status: 'archived' }) .where('id IN (:...ids)', { ids: targetIds }) .execute(); affected = result.affected || 0; } const message = skipped.length > 0 ? `成功归档 ${affected} 间;${skipped.length} 间被跳过(${skipped.slice(0, 5).join('、')}${skipped.length > 5 ? '…' : ''})` : `已批量归档 ${affected} 间宿舍(数据已保留,可随时恢复)`; return { message, archived: affected, skipped: skipped.length }; } async restore(id: number) { const room = await this.findOne(id); if (room.status !== 'archived') throw new BadRequestException('该宿舍未被归档'); await this.repo.update(id, { status: 'available' }); return { message: '已恢复' }; } async purge(id: number) { const room = await this.findOne(id); if (room.status !== 'archived') throw new BadRequestException('仅已归档宿舍可以永久删除,请先归档'); const [occupancyCount, expenseCount] = await Promise.all([ this.occRepo.count({ where: { roomId: id } }), this.roomExpRepo.count({ where: { roomId: id } }), ]); if (occupancyCount > 0) throw new BadRequestException('该宿舍存在入住记录,无法永久删除'); if (expenseCount > 0) throw new BadRequestException('该宿舍存在宿舍费用,无法永久删除'); await this.repo.delete(id); return { message: '已永久删除宿舍(不可恢复)' }; } async batchPurge(ids: number[]) { const uniqueIds = [...new Set(ids || [])]; if (uniqueIds.length === 0) throw new BadRequestException('请选择要永久删除的宿舍'); if (uniqueIds.some((id) => !Number.isInteger(id) || id <= 0)) { throw new BadRequestException('宿舍 ID 无效'); } const rooms = await this.repo.find({ where: { id: In(uniqueIds) } }); if (rooms.length !== uniqueIds.length) throw new NotFoundException('部分宿舍不存在'); const deleted: number[] = []; const skipped: string[] = []; for (const room of rooms) { if (room.status !== 'archived') { skipped.push(`${room.roomNumber}(未归档)`); continue; } const [occupancyCount, expenseCount] = await Promise.all([ this.occRepo.count({ where: { roomId: room.id } }), this.roomExpRepo.count({ where: { roomId: room.id } }), ]); if (occupancyCount > 0 || expenseCount > 0) { skipped.push(`${room.roomNumber}(存在关联数据)`); continue; } await this.repo.delete(room.id); deleted.push(room.id); } const message = skipped.length > 0 ? `已永久删除 ${deleted.length} 间;${skipped.length} 间被跳过(${skipped.slice(0, 5).join('、')}${skipped.length > 5 ? '…' : ''})` : `已永久删除 ${deleted.length} 间宿舍(不可恢复)`; return { message, deleted: deleted.length, skipped: skipped.length }; } async batchRestore(ids: number[]) { const uniqueIds = [...new Set(ids || [])]; if (uniqueIds.length === 0) throw new BadRequestException('请选择要恢复的宿舍'); if (uniqueIds.some((id) => !Number.isInteger(id) || id <= 0)) { throw new BadRequestException('宿舍 ID 无效'); } const rooms = await this.repo.find({ where: { id: In(uniqueIds) } }); if (rooms.length !== uniqueIds.length) throw new NotFoundException('部分宿舍不存在'); const targetIds = rooms.filter((room) => room.status === 'archived').map((room) => room.id); const skipped = rooms.length - targetIds.length; let restored = 0; if (targetIds.length > 0) { const result = await this.repo .createQueryBuilder() .update() .set({ status: 'available' }) .where('id IN (:...ids)', { ids: targetIds }) .execute(); restored = result.affected || 0; } return { message: `已批量恢复 ${restored} 间宿舍`, restored, skipped }; } async agentSearchRooms(query: { building?: string; keyword?: string; status?: string; limit?: number }) { return this.queries.agentSearchRooms(query); } async agentGetRoomOccupancySummary(query: { date?: string; building?: string; limit?: number }) { return this.queries.agentGetRoomOccupancySummary(query); } async getRoomVisual(asOf?: string) { return this.queries.getRoomVisual(asOf); } async batchImport( rows: { roomNumber: string; building?: string; floor?: number; capacity?: number; roomType?: string; rentalCategory?: string; monthlyRate?: number; }[], ) { return this.queries.batchImport(rows); } private createDefaultBeds(roomId: number, capacity: number): Promise { return this.queries.createDefaultBeds(roomId, capacity); } private getNextBedNumber(beds: Pick[]): number { return this.bedOps.getNextBedNumber(beds); } async getRoomBeds(roomId: number): Promise { return this.bedOps.getRoomBeds(roomId); } async getRoomAvailableBeds(roomId: number): Promise { return this.bedOps.getRoomAvailableBeds(roomId); } async createBed(roomId: number, dto: CreateBedDto): Promise { return this.bedOps.createBed(roomId, dto); } async updateBed(roomId: number, id: number, dto: UpdateBedDto): Promise { return this.bedOps.updateBed(roomId, id, dto); } async deleteBed(roomId: number, id: number): Promise { return this.bedOps.deleteBed(roomId, id); } async batchCreateBeds(roomId: number, dto: BatchCreateBedDto): Promise { return this.bedOps.batchCreateBeds(roomId, dto); } async getRoomLockers(roomId: number): Promise { return this.bedOps.getRoomLockers(roomId); } async getRoomAvailableLockers(roomId: number): Promise { return this.bedOps.getRoomAvailableLockers(roomId); } async createLocker(roomId: number, dto: CreateLockerDto): Promise { return this.bedOps.createLocker(roomId, dto); } async updateLocker(roomId: number, id: number, dto: UpdateLockerDto): Promise { return this.bedOps.updateLocker(roomId, id, dto); } async deleteLocker(roomId: number, id: number): Promise { return this.bedOps.deleteLocker(roomId, id); } async batchCreateLockers(roomId: number, dto: BatchCreateLockerDto): Promise { return this.bedOps.batchCreateLockers(roomId, dto); } }