fix: harden financial transaction boundaries
This commit is contained in:
@@ -49,125 +49,106 @@ export class OccupanciesService {
|
||||
|
||||
async checkIn(dto: CheckInDto, userId?: number) {
|
||||
this.assertDateOrder(dto.checkInDate, dto.billingStartDate, '计费起始日不能早于入住日期');
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const existing = await manager.createQueryBuilder(Occupancy, 'occupancy')
|
||||
.where('occupancy.studentId = :studentId', { studentId: dto.studentId })
|
||||
.andWhere('occupancy.checkOutDate IS NULL')
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
if (existing) throw new BadRequestException('该学生已有在住记录,请先办理退宿');
|
||||
|
||||
// 检查学生是否已有活跃入住
|
||||
const existing = await this.repo.findOne({
|
||||
where: { studentId: dto.studentId, checkOutDate: IsNull() },
|
||||
});
|
||||
if (existing) throw new BadRequestException('该学生已有在住记录,请先办理退宿');
|
||||
const room = await manager.createQueryBuilder(Room, 'room')
|
||||
.where('room.id = :roomId', { roomId: dto.roomId })
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
if (!room) throw new NotFoundException('宿舍不存在');
|
||||
if (room.status === 'archived' || room.status === 'maintenance') {
|
||||
throw new BadRequestException('该宿舍当前不可入住');
|
||||
}
|
||||
const count = await manager.count(Occupancy, { where: { roomId: dto.roomId, checkOutDate: IsNull() } });
|
||||
if (count >= room.capacity) throw new BadRequestException('宿舍已满');
|
||||
const student = await manager.findOne(Student, { where: { id: dto.studentId } });
|
||||
if (!student) throw new NotFoundException('学生不存在');
|
||||
|
||||
// 检查宿舍容量
|
||||
const room = await this.roomRepo.findOne({ where: { id: dto.roomId } });
|
||||
if (!room) throw new NotFoundException('宿舍不存在');
|
||||
if (room.status === 'archived' || room.status === 'maintenance') {
|
||||
throw new BadRequestException('该宿舍当前不可入住');
|
||||
}
|
||||
const count = await this.repo.count({ where: { roomId: dto.roomId, checkOutDate: IsNull() } });
|
||||
if (count >= room.capacity) throw new BadRequestException('宿舍已满');
|
||||
if (dto.bedId) {
|
||||
const bed = await manager.createQueryBuilder(Bed, 'bed')
|
||||
.where('bed.id = :bedId AND bed.roomId = :roomId', { bedId: dto.bedId, roomId: dto.roomId })
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
if (!bed) throw new BadRequestException('床位不存在或不属于该宿舍');
|
||||
if (bed.status !== 'available') throw new BadRequestException('该床位已被占用或维修中');
|
||||
}
|
||||
if (dto.lockerId) {
|
||||
const locker = await manager.createQueryBuilder(Locker, 'locker')
|
||||
.where('locker.id = :lockerId AND locker.roomId = :roomId', { lockerId: dto.lockerId, roomId: dto.roomId })
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
if (!locker) throw new BadRequestException('柜子不存在或不属于该宿舍');
|
||||
if (locker.status !== 'available') throw new BadRequestException('柜子已被占用或维修中');
|
||||
}
|
||||
|
||||
const student = await this.studentRepo.findOne({ where: { id: dto.studentId } });
|
||||
if (!student) throw new NotFoundException('学生不存在');
|
||||
const saved = await manager.save(manager.create(Occupancy, {
|
||||
studentId: dto.studentId,
|
||||
roomId: dto.roomId,
|
||||
checkInDate: dto.checkInDate,
|
||||
billingStartDate: dto.billingStartDate || dto.checkInDate,
|
||||
stayType: dto.stayType,
|
||||
responsibleOrganizationId: student.organizationId,
|
||||
notes: dto.notes,
|
||||
bedId: dto.bedId,
|
||||
lockerId: dto.lockerId,
|
||||
}));
|
||||
if (dto.bedId) await manager.update(Bed, dto.bedId, { status: 'occupied' });
|
||||
if (dto.lockerId) await manager.update(Locker, dto.lockerId, { status: 'occupied' });
|
||||
if (count + 1 >= room.capacity) await manager.update(Room, room.id, { status: 'full' });
|
||||
|
||||
// 床位校验
|
||||
if (dto.bedId) {
|
||||
const bed = await this.bedRepo.findOne({ where: { id: dto.bedId, roomId: dto.roomId } });
|
||||
if (!bed) throw new BadRequestException('床位不存在或不属于该宿舍');
|
||||
if (bed.status !== 'available') throw new BadRequestException('该床位已被占用或维修中');
|
||||
}
|
||||
|
||||
// 柜子校验
|
||||
if (dto.lockerId) {
|
||||
const locker = await this.lockerRepo.findOne({
|
||||
where: { id: dto.lockerId, roomId: dto.roomId },
|
||||
});
|
||||
if (!locker) throw new BadRequestException('柜子不存在或不属于该宿舍');
|
||||
if (locker.status !== 'available') throw new BadRequestException('该柜子已被占用或维修中');
|
||||
}
|
||||
|
||||
const occ = this.repo.create({
|
||||
studentId: dto.studentId,
|
||||
roomId: dto.roomId,
|
||||
checkInDate: dto.checkInDate,
|
||||
billingStartDate: dto.billingStartDate || dto.checkInDate,
|
||||
stayType: dto.stayType,
|
||||
responsibleOrganizationId: student.organizationId,
|
||||
notes: dto.notes,
|
||||
bedId: dto.bedId,
|
||||
lockerId: dto.lockerId,
|
||||
});
|
||||
const saved = await this.repo.save(occ);
|
||||
|
||||
// 更新床位/柜子状态
|
||||
if (dto.bedId) {
|
||||
await this.bedRepo.update(dto.bedId, { status: 'occupied' });
|
||||
}
|
||||
if (dto.lockerId) {
|
||||
await this.lockerRepo.update(dto.lockerId, { status: 'occupied' });
|
||||
}
|
||||
|
||||
// 更新宿舍状态
|
||||
if (count + 1 >= room.capacity) {
|
||||
await this.roomRepo.update(room.id, { status: 'full' });
|
||||
}
|
||||
|
||||
if (dto.collectDeposit) {
|
||||
const existingDeposit = await this.depositRepo.findOne({
|
||||
where: { studentId: dto.studentId },
|
||||
});
|
||||
if (existingDeposit) {
|
||||
existingDeposit.amount = Number(
|
||||
(Number(existingDeposit.amount || 0) + Number(dto.depositAmount ?? 500)).toFixed(2),
|
||||
);
|
||||
existingDeposit.status = 'paid';
|
||||
existingDeposit.paidDate = dto.checkInDate;
|
||||
existingDeposit.recordedBy = userId ?? null;
|
||||
existingDeposit.notes = '入住登记自动收取';
|
||||
await this.depositRepo.save(existingDeposit);
|
||||
} else {
|
||||
await this.depositRepo.save(
|
||||
this.depositRepo.create({
|
||||
if (dto.collectDeposit) {
|
||||
let deposit = await manager.findOne(Deposit, { where: { studentId: dto.studentId } });
|
||||
if (deposit) {
|
||||
deposit.amount = Number((Number(deposit.amount || 0) + Number(dto.depositAmount ?? 500)).toFixed(2));
|
||||
deposit.status = 'paid';
|
||||
deposit.paidDate = dto.checkInDate;
|
||||
deposit.recordedBy = userId ?? null;
|
||||
deposit.notes = '入住登记自动收取';
|
||||
} else {
|
||||
deposit = manager.create(Deposit, {
|
||||
studentId: dto.studentId,
|
||||
amount: dto.depositAmount ?? 500,
|
||||
paidDate: dto.checkInDate,
|
||||
status: 'paid',
|
||||
recordedBy: userId,
|
||||
notes: '入住登记自动收取',
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
await manager.save(deposit);
|
||||
}
|
||||
}
|
||||
|
||||
return saved;
|
||||
return saved;
|
||||
});
|
||||
}
|
||||
|
||||
async checkOut(occupancyId: number, dto: CheckOutDto) {
|
||||
const occ = await this.repo.findOne({ where: { id: occupancyId } });
|
||||
if (!occ) throw new NotFoundException('入住记录不存在');
|
||||
if (occ.checkOutDate) throw new BadRequestException('该记录已退宿');
|
||||
this.assertDateOrder(occ.checkInDate, dto.checkOutDate, '退宿日期不能早于入住日期');
|
||||
this.assertDateOrder(
|
||||
occ.billingStartDate || occ.checkInDate,
|
||||
dto.billingEndDate || dto.checkOutDate,
|
||||
'计费截止日不能早于计费起始日',
|
||||
);
|
||||
|
||||
occ.checkOutDate = dto.checkOutDate;
|
||||
occ.billingEndDate = dto.billingEndDate || dto.checkOutDate;
|
||||
occ.checkOutReason = dto.checkOutReason || '';
|
||||
await this.repo.save(occ);
|
||||
|
||||
// 释放床位/柜子
|
||||
if (occ.bedId) {
|
||||
await this.bedRepo.update(occ.bedId, { status: 'available' });
|
||||
}
|
||||
if (occ.lockerId) {
|
||||
await this.lockerRepo.update(occ.lockerId, { status: 'available' });
|
||||
}
|
||||
|
||||
// 更新宿舍状态
|
||||
await this.roomRepo.update(occ.roomId, { status: 'available' });
|
||||
|
||||
return occ;
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const occ = await manager.createQueryBuilder(Occupancy, 'occupancy')
|
||||
.where('occupancy.id = :id', { id: occupancyId })
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
if (!occ) throw new NotFoundException('入住记录不存在');
|
||||
if (occ.checkOutDate) throw new BadRequestException('该记录已退宿');
|
||||
this.assertDateOrder(occ.checkInDate, dto.checkOutDate, '退宿日期不能早于入住日期');
|
||||
this.assertDateOrder(
|
||||
occ.billingStartDate || occ.checkInDate,
|
||||
dto.billingEndDate || dto.checkOutDate,
|
||||
'计费截止日不能早于计费起始日',
|
||||
);
|
||||
occ.checkOutDate = dto.checkOutDate;
|
||||
occ.billingEndDate = dto.billingEndDate || dto.checkOutDate;
|
||||
occ.checkOutReason = dto.checkOutReason || '';
|
||||
await manager.save(occ);
|
||||
if (occ.bedId) await manager.update(Bed, occ.bedId, { status: 'available' });
|
||||
if (occ.lockerId) await manager.update(Locker, occ.lockerId, { status: 'available' });
|
||||
await manager.update(Room, occ.roomId, { status: 'available' });
|
||||
return occ;
|
||||
});
|
||||
}
|
||||
|
||||
async transferRoom(occupancyId: number, dto: TransferRoomDto) {
|
||||
@@ -175,7 +156,10 @@ export class OccupanciesService {
|
||||
await runner.connect();
|
||||
await runner.startTransaction();
|
||||
try {
|
||||
const oldOcc = await runner.manager.findOne(Occupancy, { where: { id: occupancyId } });
|
||||
const oldOcc = await runner.manager.createQueryBuilder(Occupancy, 'occupancy')
|
||||
.where('occupancy.id = :id', { id: occupancyId })
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
if (!oldOcc) throw new NotFoundException('入住记录不存在');
|
||||
if (oldOcc.checkOutDate) throw new BadRequestException('该记录已退宿');
|
||||
if (oldOcc.roomId === dto.newRoomId)
|
||||
@@ -201,7 +185,10 @@ export class OccupanciesService {
|
||||
}
|
||||
await runner.manager.update(Room, oldOcc.roomId, { status: 'available' });
|
||||
// 检查新房容量
|
||||
const newRoom = await runner.manager.findOne(Room, { where: { id: dto.newRoomId } });
|
||||
const newRoom = await runner.manager.createQueryBuilder(Room, 'room')
|
||||
.where('room.id = :roomId', { roomId: dto.newRoomId })
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
if (!newRoom) throw new NotFoundException('目标宿舍不存在');
|
||||
if (newRoom.status === 'archived' || newRoom.status === 'maintenance') {
|
||||
throw new BadRequestException('目标宿舍当前不可入住');
|
||||
@@ -213,16 +200,18 @@ export class OccupanciesService {
|
||||
|
||||
// 新床位校验
|
||||
if (dto.newBedId) {
|
||||
const newBed = await runner.manager.findOne(Bed, {
|
||||
where: { id: dto.newBedId, roomId: dto.newRoomId },
|
||||
});
|
||||
const newBed = await runner.manager.createQueryBuilder(Bed, 'bed')
|
||||
.where('bed.id = :bedId AND bed.roomId = :roomId', { bedId: dto.newBedId, roomId: dto.newRoomId })
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
if (!newBed) throw new BadRequestException('目标床位不存在或不属于目标宿舍');
|
||||
if (newBed.status !== 'available') throw new BadRequestException('目标床位已被占用');
|
||||
}
|
||||
if (dto.newLockerId) {
|
||||
const newLocker = await runner.manager.findOne(Locker, {
|
||||
where: { id: dto.newLockerId, roomId: dto.newRoomId },
|
||||
});
|
||||
const newLocker = await runner.manager.createQueryBuilder(Locker, 'locker')
|
||||
.where('locker.id = :lockerId AND locker.roomId = :roomId', { lockerId: dto.newLockerId, roomId: dto.newRoomId })
|
||||
.setLock('pessimistic_write')
|
||||
.getOne();
|
||||
if (!newLocker) throw new BadRequestException('目标柜子不存在或不属于目标宿舍');
|
||||
if (newLocker.status !== 'available') throw new BadRequestException('目标柜子已被占用');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user