From a93ba657a8b04904a6d637b8467cdd454f7bb211 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Tue, 14 Jul 2026 10:53:45 +0800 Subject: [PATCH] fix: keep room capacity and beds consistent --- apps/server/src/rooms/rooms.service.spec.ts | 136 ++++++++++++++++++++ apps/server/src/rooms/rooms.service.ts | 75 ++++++++++- 2 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 apps/server/src/rooms/rooms.service.spec.ts diff --git a/apps/server/src/rooms/rooms.service.spec.ts b/apps/server/src/rooms/rooms.service.spec.ts new file mode 100644 index 0000000..ef9d40a --- /dev/null +++ b/apps/server/src/rooms/rooms.service.spec.ts @@ -0,0 +1,136 @@ +import { BadRequestException } from '@nestjs/common'; +import { DataSource, EntityManager, Repository } from 'typeorm'; +import { Bed } from '../entities/bed.entity'; +import { Locker } from '../entities/locker.entity'; +import { Occupancy } from '../entities/occupancy.entity'; +import { Room } from '../entities/room.entity'; +import { RoomExpense } from '../entities/room-expense.entity'; +import { RoomsService } from './rooms.service'; + +describe('RoomsService — capacity consistency', () => { + const createService = (options?: { + room?: Partial; + beds?: Partial[]; + activeOccupantCount?: number; + }) => { + const room = { id: 1, capacity: 2, status: 'full', ...options?.room } as Room; + const beds = (options?.beds ?? [ + { id: 1, roomId: 1, bedNumber: '1号床' }, + { id: 2, roomId: 1, bedNumber: '2号床' }, + ]) as Bed[]; + + const roomRepo = { + findOne: jest + .fn() + .mockResolvedValueOnce(room) + .mockResolvedValue({ ...room, capacity: 4 }), + update: jest.fn().mockResolvedValue(undefined), + } as unknown as Repository; + const bedRepo = { + find: jest.fn().mockResolvedValue(beds), + create: jest.fn((value) => value), + save: jest.fn(async (value) => value), + } as unknown as Repository; + const occupancyRepo = { + count: jest.fn().mockResolvedValue(options?.activeOccupantCount ?? 2), + } as unknown as Repository; + + const manager = { + getRepository: jest.fn((entity) => { + if (entity === Room) return roomRepo; + if (entity === Bed) return bedRepo; + if (entity === Occupancy) return occupancyRepo; + throw new Error(`Unexpected repository: ${String(entity)}`); + }), + } as unknown as EntityManager; + const dataSource = { + transaction: jest.fn(async (callback) => callback(manager)), + } as unknown as DataSource; + + const service = new RoomsService( + roomRepo, + occupancyRepo, + {} as Repository, + bedRepo, + {} as Repository, + dataSource, + ); + + return { service, roomRepo, bedRepo, occupancyRepo }; + }; + + it('automatically creates missing beds when capacity increases', async () => { + const { service, roomRepo, bedRepo } = createService(); + + await service.update(1, { capacity: 4 }); + + expect(bedRepo.create).toHaveBeenNthCalledWith(1, { roomId: 1, bedNumber: '3号床' }); + expect(bedRepo.create).toHaveBeenNthCalledWith(2, { roomId: 1, bedNumber: '4号床' }); + expect(bedRepo.save).toHaveBeenCalledWith([ + { roomId: 1, bedNumber: '3号床' }, + { roomId: 1, bedNumber: '4号床' }, + ]); + expect(roomRepo.update).toHaveBeenCalledWith(1, { capacity: 4, status: 'available' }); + }); + + it('rejects capacity lower than the active occupant count', async () => { + const { service, roomRepo, bedRepo } = createService({ + room: { capacity: 4, status: 'available' }, + beds: [{ id: 1, roomId: 1, bedNumber: '1号床' }], + activeOccupantCount: 3, + }); + + await expect(service.update(1, { capacity: 2 })).rejects.toThrow( + new BadRequestException('额定人数不能少于当前入住人数,当前有 3 人入住'), + ); + expect(roomRepo.update).not.toHaveBeenCalled(); + expect(bedRepo.save).not.toHaveBeenCalled(); + }); + + it('rejects capacity lower than the existing bed count', async () => { + const { service, roomRepo, bedRepo } = createService({ + room: { capacity: 4, status: 'available' }, + activeOccupantCount: 1, + beds: [ + { id: 1, roomId: 1, bedNumber: '1号床' }, + { id: 2, roomId: 1, bedNumber: '2号床' }, + { id: 3, roomId: 1, bedNumber: '3号床' }, + { id: 4, roomId: 1, bedNumber: '4号床' }, + ], + }); + + await expect(service.update(1, { capacity: 3 })).rejects.toThrow( + new BadRequestException( + '额定人数不能少于现有床位数,当前有 4 张床位,请先删除多余的空闲床位', + ), + ); + expect(roomRepo.update).not.toHaveBeenCalled(); + expect(bedRepo.save).not.toHaveBeenCalled(); + }); + + it('marks the room full when a valid capacity reduction reaches the occupant count', async () => { + const { service, roomRepo, bedRepo } = createService({ + room: { capacity: 4, status: 'available' }, + activeOccupantCount: 2, + beds: [ + { id: 1, roomId: 1, bedNumber: '1号床' }, + { id: 2, roomId: 1, bedNumber: '2号床' }, + ], + }); + + await service.update(1, { capacity: 2 }); + + expect(roomRepo.update).toHaveBeenCalledWith(1, { capacity: 2, status: 'full' }); + expect(bedRepo.save).not.toHaveBeenCalled(); + }); + + it('updates other room fields without changing beds', async () => { + const { service, roomRepo, bedRepo, occupancyRepo } = createService(); + + await service.update(1, { building: '2号楼' }); + + expect(roomRepo.update).toHaveBeenCalledWith(1, { building: '2号楼' }); + expect(bedRepo.find).not.toHaveBeenCalled(); + expect(occupancyRepo.count).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/server/src/rooms/rooms.service.ts b/apps/server/src/rooms/rooms.service.ts index b7c9970..647ab9b 100644 --- a/apps/server/src/rooms/rooms.service.ts +++ b/apps/server/src/rooms/rooms.service.ts @@ -1,6 +1,15 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, Like, IsNull, Not, In, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; +import { + DataSource, + Repository, + Like, + IsNull, + Not, + In, + LessThanOrEqual, + MoreThanOrEqual, +} from 'typeorm'; import { Room } from '../entities/room.entity'; import { Occupancy } from '../entities/occupancy.entity'; @@ -19,6 +28,7 @@ export class RoomsService { @InjectRepository(RoomExpense) private roomExpRepo: Repository, @InjectRepository(Bed) private bedRepo: Repository, @InjectRepository(Locker) private lockerRepo: Repository, + private dataSource: DataSource, ) {} /** @@ -116,9 +126,54 @@ export class RoomsService { } async update(id: number, dto: UpdateRoomDto) { - await this.findOne(id); - await this.repo.update(id, dto); - return this.repo.findOne({ where: { id } }); + 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) { @@ -413,6 +468,14 @@ export class RoomsService { await this.bedRepo.save(beds); } + private getNextBedNumber(beds: Pick[]): number { + const numbers = beds.map((bed) => { + const match = bed.bedNumber.match(/^\d+/); + return match ? parseInt(match[0], 10) : 0; + }); + return numbers.length > 0 ? Math.max(...numbers) + 1 : 1; + } + private async assertCanAddBeds(room: Room, count: number): Promise { const existingCount = await this.bedRepo.count({ where: { roomId: room.id } }); this.assertCanAddBedsFromCount(room, existingCount, count); @@ -421,7 +484,9 @@ export class RoomsService { private assertCanAddBedsFromCount(room: Room, existingCount: number, count: number): void { const remaining = Math.max((room.capacity ?? 0) - existingCount, 0); if (count > remaining) { - throw new BadRequestException(`床位不能超过额定人数,当前已有 ${existingCount} 张,额定 ${room.capacity} 张,最多还能添加 ${remaining} 张`); + throw new BadRequestException( + `床位不能超过额定人数,当前已有 ${existingCount} 张,额定 ${room.capacity} 张,最多还能添加 ${remaining} 张`, + ); } } -- 2.49.1