feat: 重构各业务模块管理页面与服务

This commit is contained in:
2026-08-05 17:12:00 +08:00
parent 80e6fccf05
commit fd39e1686a
163 changed files with 18409 additions and 13449 deletions

View File

@@ -1,14 +1,6 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { Injectable, NotFoundException, BadRequestException, Optional } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
DataSource,
Repository,
Like,
IsNull,
Not,
In,
LessThanOrEqual,
} from 'typeorm';
import { DataSource, Repository, IsNull, Not, In } from 'typeorm';
import { Room } from '../entities/room.entity';
import { Occupancy } from '../entities/occupancy.entity';
@@ -19,26 +11,9 @@ 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 { occupancyWhereOnDate } from './room-occupancy-date';
interface AgentRoomRow {
id: string | number;
roomNumber: string;
building: string | null;
floor: string | number | null;
capacity: string | number;
roomType: string | null;
status: string;
occupiedBeds: string | number;
}
interface AgentRoomOccupancyRow {
roomId: string | number;
roomNumber: string;
building: string | null;
capacity: string | number;
occupiedBeds: string | number;
}
import { RoomQueryService } from './room-query.service';
import { RoomBedLockerService } from './room-bed-locker.service';
import { parseRoomNumber } from './room-number';
@Injectable()
export class RoomsService {
@@ -50,8 +25,24 @@ export class RoomsService {
@InjectRepository(Locker) private lockerRepo: Repository<Locker>,
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:"四人间"
@@ -59,42 +50,8 @@ export class RoomsService {
* "3-301" → building:"3号楼", floor:3, roomType:"四人间"
* "8-102" → building:"8号楼", floor:1, roomType:"爆改房"
*/
static parseRoomNumber(roomNumber: string): {
building?: string;
floor?: number;
roomType?: string;
capacity?: number;
} {
const cleaned = roomNumber.replace(/[(].*?[)]/g, '').trim();
// 家庭房: X-Y-ZZZ 格式
const familyMatch = cleaned.match(/^(\d+)-(\d+)-(\d+)$/);
if (familyMatch) {
const bldg = `${familyMatch[1]}-${familyMatch[2]}`;
const roomPart = familyMatch[3];
const rawFloor = parseInt(roomPart.charAt(0), 10);
const floor = Number.isNaN(rawFloor) ? undefined : rawFloor;
return { building: bldg, floor, roomType: '家庭房', capacity: 4 };
}
// 标准: X-YZZ 格式
const stdMatch = cleaned.match(/^(\d+)-(\d+)$/);
if (stdMatch) {
const bldgNum = stdMatch[1];
const roomPart = stdMatch[2];
const rawFloor = parseInt(roomPart.charAt(0), 10);
const floor = Number.isNaN(rawFloor) ? undefined : rawFloor;
const building = `${bldgNum}号楼`;
let roomType = '四人间';
let capacity = 4;
if (bldgNum === '2') {
roomType = '单人间';
capacity = 1;
} else if (bldgNum === '8') {
roomType = '爆改房';
capacity = 2;
}
return { building, floor, roomType, capacity };
}
return { capacity: 4, roomType: '四人间' };
static parseRoomNumber(roomNumber: string) {
return parseRoomNumber(roomNumber);
}
async findAll(query?: { building?: string; includeArchived?: boolean }) {
@@ -104,59 +61,6 @@ export class RoomsService {
return this.repo.find({ where, order: { roomNumber: 'ASC' } });
}
async agentSearchRooms(query: { keyword?: string; building?: string; status?: string; limit?: number }) {
const qb = this.repo
.createQueryBuilder('room')
.leftJoin(
Occupancy,
'occupancy',
'occupancy.roomId = room.id AND occupancy.checkOutDate IS NULL',
)
.select('room.id', 'id')
.addSelect('room.roomNumber', 'roomNumber')
.addSelect('room.building', 'building')
.addSelect('room.floor', 'floor')
.addSelect('room.capacity', 'capacity')
.addSelect('room.roomType', 'roomType')
.addSelect('room.status', 'status')
.addSelect('COUNT(occupancy.id)', 'occupiedBeds')
.where('room.status != :archived', { archived: 'archived' });
if (query.keyword) qb.andWhere('room.roomNumber LIKE :keyword', { keyword: `%${query.keyword}%` });
if (query.building) qb.andWhere('room.building = :building', { building: query.building });
if (query.status) qb.andWhere('room.status = :status', { status: query.status });
const rows = await qb.groupBy('room.id').orderBy('room.roomNumber', 'ASC').limit(query.limit ?? 20).getRawMany<AgentRoomRow>();
return rows.map((row) => ({
...row,
id: Number(row.id), floor: row.floor == null ? null : Number(row.floor),
capacity: Number(row.capacity), occupiedBeds: Number(row.occupiedBeds || 0),
}));
}
async agentGetRoomOccupancySummary(query: { date?: string; building?: string; limit?: number }) {
const targetDate = query.date || this.getChinaDate(new Date());
const qb = this.repo
.createQueryBuilder('room')
.leftJoin(
Occupancy,
'occupancy',
'occupancy.roomId = room.id AND occupancy.checkInDate <= :targetDate AND (occupancy.checkOutDate IS NULL OR occupancy.checkOutDate > :targetDate)',
{ targetDate },
)
.select('room.id', 'roomId')
.addSelect('room.roomNumber', 'roomNumber')
.addSelect('room.building', 'building')
.addSelect('room.capacity', 'capacity')
.addSelect('COUNT(occupancy.id)', 'occupiedBeds')
.where('room.status != :archived', { archived: 'archived' });
if (query.building) qb.andWhere('room.building = :building', { building: query.building });
const rows = await qb.groupBy('room.id').orderBy('room.roomNumber', 'ASC').limit(query.limit ?? 50).getRawMany<AgentRoomOccupancyRow>();
return rows.map((row) => {
const capacity = Number(row.capacity || 0);
const occupiedBeds = Number(row.occupiedBeds || 0);
return { date: targetDate, roomId: Number(row.roomId), roomNumber: row.roomNumber, building: row.building, capacity, occupiedBeds, availableBeds: Math.max(0, capacity - occupiedBeds) };
});
}
async findOne(id: number) {
const room = await this.repo.findOne({ where: { id } });
if (!room) throw new NotFoundException('宿舍不存在');
@@ -306,6 +210,54 @@ export class RoomsService {
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('请选择要恢复的宿舍');
@@ -329,147 +281,16 @@ export class RoomsService {
}
return { message: `已批量恢复 ${restored} 间宿舍`, restored, skipped };
}
async getRoomVisual(asOf?: string) {
// asOf 为空 = 实时(今天)。带 asOf = 还原该日期结束时的历史入住快照。
const isHistorical = !!asOf;
const targetDate = asOf || this.getChinaDate(new Date());
// 实时视图排除已归档房间;历史视图不排除——当时有人住的房间即使现在已归档也应显示。
const rooms = await this.repo.find({
where: isHistorical ? {} : { status: Not('archived') },
order: { building: 'ASC', roomNumber: 'ASC' },
});
const occupancies = await this.occRepo.find({
where: occupancyWhereOnDate(targetDate),
relations: ['student', 'student.organization', 'responsibleOrganization', 'bed'],
order: { checkInDate: 'ASC' },
});
// 按roomId分组入住记录
const occMap = new Map<number, any[]>();
// days已住天数相对目标日期计算而非固定今天历史视图才准确。
const refTime = new Date(targetDate).getTime();
for (const occ of occupancies) {
if (!occMap.has(occ.roomId)) occMap.set(occ.roomId, []);
const checkIn = new Date(occ.checkInDate);
const days = Math.max(1, Math.ceil((refTime - checkIn.getTime()) / (1000 * 60 * 60 * 24)));
occMap.get(occ.roomId)!.push({
studentId: occ.studentId,
occupancyId: occ.id,
studentName: occ.student?.name || '未知',
bedId: occ.bedId ?? null,
bedNumber: occ.bed?.bedNumber || null,
checkInDate: occ.checkInDate,
billingStartDate: occ.billingStartDate,
days,
organization: occ.student?.organization?.name || null,
supervisor: occ.student?.supervisor || null,
organizationId: occ.responsibleOrganizationId || null,
organizationName: occ.responsibleOrganization?.name || null,
organizationColor: occ.responsibleOrganization?.color || null,
});
}
// 获取各楼栋列表
const buildings = [...new Set(rooms.map((r) => r.building).filter(Boolean))];
// 历史视图纳入了已归档房间,但只保留当时确实有人住的归档房间,避免空归档房间刷屏。
const visibleRooms = isHistorical
? rooms.filter((r) => r.status !== 'archived' || (occMap.get(r.id)?.length ?? 0) > 0)
: rooms;
// 批量获取床位统计
const allBeds = await this.bedRepo.find({
where: { roomId: In(visibleRooms.map((r) => r.id)) },
});
const bedMap = new Map<number, { total: number; occupied: number }>();
for (const bed of allBeds) {
if (!bedMap.has(bed.roomId)) bedMap.set(bed.roomId, { total: 0, occupied: 0 });
const entry = bedMap.get(bed.roomId)!;
entry.total++;
if (bed.status === 'occupied') entry.occupied++;
}
const inspectionMap = await this.inspectionsService.getByRoomsAndDate(
visibleRooms.map((room) => room.id),
targetDate,
);
return {
buildings,
rooms: visibleRooms.map((room) => {
const occ = occMap.get(room.id) || [];
const inspection = inspectionMap.get(room.id);
const inspectionByOccupancyId = new Map(
(inspection?.details || []).map((detail) => [detail.occupancyId, detail.status]),
);
const orgs = [...new Set(occ.map((o: any) => o.organization).filter(Boolean))];
let orgLabel: string | null = null;
if (orgs.length > 0 && occ.length > 0) {
const allSameOrg = occ.every((o: any) => o.organization && o.organization === orgs[0]);
orgLabel = allSameOrg ? `均为${orgs[0]}人员` : `存在${orgs.join('、')}人员`;
}
const organizationColors = [
...new Set(occ.map((o: any) => o.organizationColor).filter(Boolean)),
];
const organizationColor: string | null =
organizationColors.length === 1 ? organizationColors[0] : null;
const organizationIds = [...new Set(occ.map((o: any) => o.organizationId).filter(Boolean))];
return {
id: room.id,
roomNumber: room.roomNumber,
building: room.building,
floor: room.floor,
capacity: room.capacity,
status: room.status,
currentCount: occ.length,
totalBeds: bedMap.get(room.id)?.total ?? 0,
occupiedBeds: bedMap.get(room.id)?.occupied ?? 0,
occupants: occ.map((occupant) => ({
...occupant,
inspectionStatus: inspectionByOccupancyId.get(occupant.occupancyId) || null,
})),
inspection: inspection
? {
submitted: true,
inspectorId: inspection.inspectorId,
inspectorName: inspection.inspectorName,
source: inspection.source,
submittedAt: inspection.submittedAt,
}
: { submitted: false },
orgLabel,
organizationColor,
organizationIds,
};
}),
// 当前视图内出现过的负责机构,供筛选下拉使用
organizations: [
...new Map(
occupancies
.filter((o) => o.responsibleOrganizationId && o.responsibleOrganization)
.map((o) => [
o.responsibleOrganizationId,
{
id: o.responsibleOrganizationId,
name: o.responsibleOrganization.name,
color: o.responsibleOrganization.color || null,
},
]),
).values(),
].sort((a, b) => a.name.localeCompare(b.name)),
};
async agentSearchRooms(query: { building?: string; keyword?: string; status?: string; limit?: number }) {
return this.queries.agentSearchRooms(query);
}
private getChinaDate(now: Date): string {
return new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(now);
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(
@@ -483,212 +304,63 @@ export class RoomsService {
monthlyRate?: number;
}[],
) {
let imported = 0;
let skipped = 0;
for (const row of rows) {
if (!row.roomNumber || !row.roomNumber.trim()) {
skipped++;
continue;
}
const exists = await this.repo.findOne({ where: { roomNumber: row.roomNumber.trim() } });
if (exists) {
skipped++;
continue;
}
// 智能解析房间号
const parsed = RoomsService.parseRoomNumber(row.roomNumber.trim());
const room = await this.repo.save(
this.repo.create({
roomNumber: row.roomNumber.trim(),
building: row.building?.trim() || parsed.building || undefined,
floor: row.floor ?? parsed.floor,
capacity: row.capacity ?? parsed.capacity ?? 4,
roomType: row.roomType || parsed.roomType || undefined,
rentalCategory: row.rentalCategory || undefined,
monthlyRate: row.monthlyRate ?? undefined,
}),
);
await this.createDefaultBeds(room.id, room.capacity);
imported++;
}
return {
message: `成功导入 ${imported} 间宿舍,跳过 ${skipped} 条(重复或空行)`,
imported,
skipped,
};
return this.queries.batchImport(rows);
}
// ── 床位管理 ──
async getRoomBeds(roomId: number): Promise<Bed[]> {
const room = await this.repo.findOne({ where: { id: roomId } });
if (!room) throw new NotFoundException('宿舍不存在');
return this.bedRepo.find({ where: { roomId, status: Not('archived') }, order: { bedNumber: 'ASC' } });
}
async getRoomAvailableBeds(roomId: number): Promise<Bed[]> {
const room = await this.repo.findOne({ where: { id: roomId } });
if (!room) throw new NotFoundException('宿舍不存在');
return this.bedRepo.find({
where: { roomId, status: 'available' },
order: { bedNumber: 'ASC' },
});
}
async createBed(roomId: number, dto: CreateBedDto): Promise<Bed> {
const room = await this.repo.findOne({ where: { id: roomId } });
if (!room) throw new NotFoundException('宿舍不存在');
if (room.status === 'archived') throw new BadRequestException('已归档宿舍不能添加床位');
await this.assertCanAddBeds(room, 1);
const existing = await this.bedRepo.findOne({ where: { roomId, bedNumber: dto.bedNumber } });
if (existing) throw new BadRequestException('该床位编号已存在');
const bed = this.bedRepo.create({ ...dto, roomId });
return this.bedRepo.save(bed);
}
async updateBed(roomId: number, id: number, dto: UpdateBedDto): Promise<Bed> {
const bed = await this.bedRepo.findOne({ where: { id, roomId } });
if (!bed) throw new NotFoundException('床位不存在');
// 不允许将 occupied 的床位改为 maintenance
if (dto.status === 'maintenance' && bed.status === 'occupied') {
throw new BadRequestException('该床位有人入住,请先退宿');
}
// 编号唯一性检查
if (dto.bedNumber && dto.bedNumber !== bed.bedNumber) {
const dup = await this.bedRepo.findOne({ where: { roomId, bedNumber: dto.bedNumber } });
if (dup) throw new BadRequestException('该床位编号已存在');
}
Object.assign(bed, dto);
return this.bedRepo.save(bed);
}
async deleteBed(roomId: number, id: number): Promise<void> {
const bed = await this.bedRepo.findOne({ where: { id, roomId } });
if (!bed) throw new NotFoundException('床位不存在');
if (bed.status === 'occupied') throw new BadRequestException('该床位有人入住,无法归档');
if (bed.status === 'archived') throw new BadRequestException('该床位已归档');
await this.bedRepo.update(id, { status: 'archived' });
}
async batchCreateBeds(roomId: number, dto: BatchCreateBedDto): Promise<Bed[]> {
const room = await this.repo.findOne({ where: { id: roomId } });
if (!room) throw new NotFoundException('宿舍不存在');
if (room.status === 'archived') throw new BadRequestException('已归档宿舍不能添加床位');
const existing = await this.bedRepo.find({ where: { roomId, status: Not('archived') }, order: { bedNumber: 'ASC' } });
this.assertCanAddBedsFromCount(room, existing.length, dto.count);
const numbers = existing.map((b) => {
const match = b.bedNumber.match(/^\d+/);
return match ? parseInt(match[0]) : 0;
});
const start = numbers.length > 0 ? Math.max(...numbers) + 1 : 1;
const beds: Bed[] = [];
for (let i = 0; i < dto.count; i++) {
beds.push(this.bedRepo.create({ roomId, bedNumber: `${start + i}号床` }));
}
return this.bedRepo.save(beds);
}
private async createDefaultBeds(roomId: number, capacity: number): Promise<void> {
const count = Math.max(capacity ?? 0, 0);
if (count === 0) return;
const beds = Array.from({ length: count }, (_, index) =>
this.bedRepo.create({ roomId, bedNumber: `${index + 1}号床` }),
);
await this.bedRepo.save(beds);
private createDefaultBeds(roomId: number, capacity: number): Promise<void> {
return this.queries.createDefaultBeds(roomId, capacity);
}
private getNextBedNumber(beds: Pick<Bed, 'bedNumber'>[]): 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;
return this.bedOps.getNextBedNumber(beds);
}
private async assertCanAddBeds(room: Room, count: number): Promise<void> {
const existingCount = await this.bedRepo.count({ where: { roomId: room.id } });
this.assertCanAddBedsFromCount(room, existingCount, count);
async getRoomBeds(roomId: number): Promise<Bed[]> {
return this.bedOps.getRoomBeds(roomId);
}
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}`,
);
}
async getRoomAvailableBeds(roomId: number): Promise<Bed[]> {
return this.bedOps.getRoomAvailableBeds(roomId);
}
// ── 柜子管理 ──
async createBed(roomId: number, dto: CreateBedDto): Promise<Bed> {
return this.bedOps.createBed(roomId, dto);
}
async updateBed(roomId: number, id: number, dto: UpdateBedDto): Promise<Bed> {
return this.bedOps.updateBed(roomId, id, dto);
}
async deleteBed(roomId: number, id: number): Promise<void> {
return this.bedOps.deleteBed(roomId, id);
}
async batchCreateBeds(roomId: number, dto: BatchCreateBedDto): Promise<Bed[]> {
return this.bedOps.batchCreateBeds(roomId, dto);
}
async getRoomLockers(roomId: number): Promise<Locker[]> {
const room = await this.repo.findOne({ where: { id: roomId } });
if (!room) throw new NotFoundException('宿舍不存在');
return this.lockerRepo.find({ where: { roomId, status: Not('archived') }, order: { lockerNumber: 'ASC' } });
return this.bedOps.getRoomLockers(roomId);
}
async getRoomAvailableLockers(roomId: number): Promise<Locker[]> {
const room = await this.repo.findOne({ where: { id: roomId } });
if (!room) throw new NotFoundException('宿舍不存在');
return this.lockerRepo.find({
where: { roomId, status: 'available' },
order: { lockerNumber: 'ASC' },
});
return this.bedOps.getRoomAvailableLockers(roomId);
}
async createLocker(roomId: number, dto: CreateLockerDto): Promise<Locker> {
const room = await this.repo.findOne({ where: { id: roomId } });
if (!room) throw new NotFoundException('宿舍不存在');
if (room.status === 'archived') throw new BadRequestException('已归档宿舍不能添加柜子');
const existing = await this.lockerRepo.findOne({
where: { roomId, lockerNumber: dto.lockerNumber },
});
if (existing) throw new BadRequestException('该柜子编号已存在');
const locker = this.lockerRepo.create({ ...dto, roomId });
return this.lockerRepo.save(locker);
return this.bedOps.createLocker(roomId, dto);
}
async updateLocker(roomId: number, id: number, dto: UpdateLockerDto): Promise<Locker> {
const locker = await this.lockerRepo.findOne({ where: { id, roomId } });
if (!locker) throw new NotFoundException('柜子不存在');
if (dto.status === 'maintenance' && locker.status === 'occupied') {
throw new BadRequestException('该柜子有人占用,请先释放');
}
if (dto.lockerNumber && dto.lockerNumber !== locker.lockerNumber) {
const dup = await this.lockerRepo.findOne({
where: { roomId, lockerNumber: dto.lockerNumber },
});
if (dup) throw new BadRequestException('该柜子编号已存在');
}
Object.assign(locker, dto);
return this.lockerRepo.save(locker);
return this.bedOps.updateLocker(roomId, id, dto);
}
async deleteLocker(roomId: number, id: number): Promise<void> {
const locker = await this.lockerRepo.findOne({ where: { id, roomId } });
if (!locker) throw new NotFoundException('柜子不存在');
if (locker.status === 'occupied') throw new BadRequestException('该柜子有人占用,无法归档');
if (locker.status === 'archived') throw new BadRequestException('该柜子已归档');
await this.lockerRepo.update(id, { status: 'archived' });
return this.bedOps.deleteLocker(roomId, id);
}
async batchCreateLockers(roomId: number, dto: BatchCreateLockerDto): Promise<Locker[]> {
const room = await this.repo.findOne({ where: { id: roomId } });
if (!room) throw new NotFoundException('宿舍不存在');
if (room.status === 'archived') throw new BadRequestException('已归档宿舍不能添加柜子');
const existing = await this.lockerRepo.find({
where: { roomId, status: Not('archived') },
order: { lockerNumber: 'ASC' },
});
const numbers = existing.map((b) => {
const match = b.lockerNumber.match(/^\d+/);
return match ? parseInt(match[0]) : 0;
});
const start = numbers.length > 0 ? Math.max(...numbers) + 1 : 1;
const lockers: Locker[] = [];
for (let i = 0; i < dto.count; i++) {
lockers.push(this.lockerRepo.create({ roomId, lockerNumber: `${start + i}号柜` }));
}
return this.lockerRepo.save(lockers);
return this.bedOps.batchCreateLockers(roomId, dto);
}
}
}