fix: harden financial transaction boundaries

This commit is contained in:
2026-07-18 15:33:41 +08:00
parent 4af7acbeaa
commit 5836b421d8
19 changed files with 514 additions and 314 deletions

View File

@@ -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('目标柜子已被占用');
}