feat: 重构各业务模块管理页面与服务
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user