feat: 增加宿舍查寝功能

This commit is contained in:
2026-07-22 10:55:48 +08:00
parent c888dce065
commit 54e283b687
19 changed files with 923 additions and 18 deletions

View File

@@ -19,6 +19,7 @@ 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';
@Injectable()
export class RoomsService {
@@ -29,6 +30,7 @@ export class RoomsService {
@InjectRepository(Bed) private bedRepo: Repository<Bed>,
@InjectRepository(Locker) private lockerRepo: Repository<Locker>,
private dataSource: DataSource,
private readonly inspectionsService: RoomInspectionsService,
) {}
/**
@@ -235,7 +237,7 @@ export class RoomsService {
async getRoomVisual(asOf?: string) {
// asOf 为空 = 实时(今天)。带 asOf = 还原该日期结束时的历史入住快照。
const isHistorical = !!asOf;
const targetDate = asOf || new Date().toISOString().slice(0, 10);
const targetDate = asOf || this.getChinaDate(new Date());
// 实时视图排除已归档房间;历史视图不排除——当时有人住的房间即使现在已归档也应显示。
const rooms = await this.repo.find({
@@ -244,13 +246,11 @@ export class RoomsService {
});
const occupancies = await this.occRepo.find({
where: isHistorical
? [
{ checkInDate: LessThanOrEqual(targetDate), checkOutDate: IsNull() },
{ checkInDate: LessThanOrEqual(targetDate), checkOutDate: MoreThanOrEqual(targetDate) },
]
: { checkOutDate: IsNull() },
relations: ['student', 'student.organization', 'responsibleOrganization'],
where: [
{ checkInDate: LessThanOrEqual(targetDate), checkOutDate: IsNull() },
{ checkInDate: LessThanOrEqual(targetDate), checkOutDate: MoreThanOrEqual(targetDate) },
],
relations: ['student', 'student.organization', 'responsibleOrganization', 'bed'],
order: { checkInDate: 'ASC' },
});
@@ -264,7 +264,10 @@ export class RoomsService {
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,
@@ -296,10 +299,19 @@ export class RoomsService {
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) {
@@ -322,7 +334,19 @@ export class RoomsService {
currentCount: occ.length,
totalBeds: bedMap.get(room.id)?.total ?? 0,
occupiedBeds: bedMap.get(room.id)?.occupied ?? 0,
occupants: occ,
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,
@@ -346,6 +370,15 @@ export class RoomsService {
};
}
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 batchImport(
rows: {
roomNumber: string;