From 7ffc573d2e06a4a249b7932370672ec60051cabd Mon Sep 17 00:00:00 2001 From: xiong Date: Wed, 22 Jul 2026 15:03:06 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20SQLite=20=E9=92=B1?= =?UTF-8?q?=E5=8C=85=E8=B0=83=E8=B4=A6=E9=94=81=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/wallets/wallets.service.spec.ts | 51 +++++++++++++++++++ apps/server/src/wallets/wallets.service.ts | 11 ++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/apps/server/src/wallets/wallets.service.spec.ts b/apps/server/src/wallets/wallets.service.spec.ts index c730c62..adf919f 100644 --- a/apps/server/src/wallets/wallets.service.spec.ts +++ b/apps/server/src/wallets/wallets.service.spec.ts @@ -117,3 +117,54 @@ describe('WalletsService financial boundaries', () => { expect(dataSource.transaction).not.toHaveBeenCalled(); }); }); + +describe('WalletsService wallet locking', () => { + const createQueryManager = () => { + const wallet = { id: 1, studentId: 10, balance: 0 }; + const query = { + where: jest.fn(), + setLock: jest.fn(), + getOne: jest.fn(async () => wallet), + }; + query.where.mockReturnValue(query); + query.setLock.mockReturnValue(query); + return { + wallet, + query, + manager: { + createQueryBuilder: jest.fn(() => query), + findOne: jest.fn(), + }, + }; + }; + + it('skips pessimistic locking for SQLite', async () => { + const ctx = createQueryManager(); + const service = new WalletsService( + {} as any, + {} as any, + {} as any, + { options: { type: 'better-sqlite3' } } as any, + ); + + const result = await (service as any).getOrCreateWallet(ctx.manager, 10, true); + + expect(result).toBe(ctx.wallet); + expect(ctx.query.setLock).not.toHaveBeenCalled(); + expect(ctx.query.getOne).toHaveBeenCalled(); + }); + + it('keeps pessimistic write locking for MySQL', async () => { + const ctx = createQueryManager(); + const service = new WalletsService( + {} as any, + {} as any, + {} as any, + { options: { type: 'mysql' } } as any, + ); + + await (service as any).getOrCreateWallet(ctx.manager, 10, true); + + expect(ctx.query.setLock).toHaveBeenCalledWith('pessimistic_write'); + }); +}); diff --git a/apps/server/src/wallets/wallets.service.ts b/apps/server/src/wallets/wallets.service.ts index 1078446..270d2de 100644 --- a/apps/server/src/wallets/wallets.service.ts +++ b/apps/server/src/wallets/wallets.service.ts @@ -259,10 +259,13 @@ export class WalletsService { if (!lock || !manager.createQueryBuilder) { return manager.findOne(StudentWallet, { where: { studentId } }); } - return manager.createQueryBuilder(StudentWallet, 'wallet') - .where('wallet.studentId = :studentId', { studentId }) - .setLock('pessimistic_write') - .getOne(); + let query = manager + .createQueryBuilder(StudentWallet, 'wallet') + .where('wallet.studentId = :studentId', { studentId }); + if (['mysql', 'mariadb', 'postgres', 'cockroachdb'].includes(this.dataSource.options.type)) { + query = query.setLock('pessimistic_write'); + } + return query.getOne(); }; let wallet = await find(); if (!wallet) { From a9009f86fb38a0f889cdd1d305edb018191ec690 Mon Sep 17 00:00:00 2001 From: xiong Date: Wed, 22 Jul 2026 15:18:12 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E6=8D=A2=E5=AE=BF?= =?UTF-8?q?=E5=BD=93=E5=A4=A9=E6=9F=A5=E5=AF=9D=E5=BD=92=E5=B1=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/rooms/room-inspections.service.ts | 13 ++++------- .../src/rooms/room-occupancy-date.spec.ts | 22 +++++++++++++++++++ apps/server/src/rooms/room-occupancy-date.ts | 13 +++++++++++ apps/server/src/rooms/rooms.service.ts | 7 ++---- 4 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 apps/server/src/rooms/room-occupancy-date.spec.ts create mode 100644 apps/server/src/rooms/room-occupancy-date.ts diff --git a/apps/server/src/rooms/room-inspections.service.ts b/apps/server/src/rooms/room-inspections.service.ts index 5226b55..5283499 100644 --- a/apps/server/src/rooms/room-inspections.service.ts +++ b/apps/server/src/rooms/room-inspections.service.ts @@ -1,13 +1,14 @@ import { BadRequestException, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { InjectRepository } from '@nestjs/typeorm'; -import { DataSource, EntityManager, IsNull, LessThanOrEqual, MoreThanOrEqual, Repository } from 'typeorm'; +import { DataSource, EntityManager, Repository } from 'typeorm'; import { Bed } from '../entities/bed.entity'; import { Occupancy } from '../entities/occupancy.entity'; import { Room } from '../entities/room.entity'; import { RoomInspection } from '../entities/room-inspection.entity'; import { RoomInspectionDetail } from '../entities/room-inspection-detail.entity'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; +import { occupancyWhereOnDate } from './room-occupancy-date'; interface InspectorIdentity { id?: number; @@ -211,10 +212,7 @@ export class RoomInspectionsService implements OnApplicationBootstrap { private findOccupanciesForDate(manager: EntityManager, roomId: number, date: string) { return manager.getRepository(Occupancy).find({ - where: [ - { roomId, checkInDate: LessThanOrEqual(date), checkOutDate: IsNull() }, - { roomId, checkInDate: LessThanOrEqual(date), checkOutDate: MoreThanOrEqual(date) }, - ], + where: occupancyWhereOnDate(date, roomId), relations: ['student', 'bed'], order: { id: 'ASC' }, }); @@ -222,10 +220,7 @@ export class RoomInspectionsService implements OnApplicationBootstrap { private findAllOccupanciesForDate(manager: EntityManager, date: string) { return manager.getRepository(Occupancy).find({ - where: [ - { checkInDate: LessThanOrEqual(date), checkOutDate: IsNull() }, - { checkInDate: LessThanOrEqual(date), checkOutDate: MoreThanOrEqual(date) }, - ], + where: occupancyWhereOnDate(date), relations: ['student', 'bed'], order: { roomId: 'ASC', id: 'ASC' }, }); diff --git a/apps/server/src/rooms/room-occupancy-date.spec.ts b/apps/server/src/rooms/room-occupancy-date.spec.ts new file mode 100644 index 0000000..cf6fb25 --- /dev/null +++ b/apps/server/src/rooms/room-occupancy-date.spec.ts @@ -0,0 +1,22 @@ +import { occupancyWhereOnDate } from './room-occupancy-date'; + +describe('occupancyWhereOnDate', () => { + it('treats checkout date as the first day no longer occupying the old room', () => { + const where = occupancyWhereOnDate('2026-07-22', 10); + + expect(where).toHaveLength(2); + expect(where[0]).toEqual( + expect.objectContaining({ roomId: 10, checkInDate: expect.anything(), checkOutDate: expect.anything() }), + ); + expect(where[1]).toEqual( + expect.objectContaining({ roomId: 10, checkInDate: expect.anything(), checkOutDate: expect.anything() }), + ); + expect(String((where[1] as any).checkOutDate._type)).toBe('moreThan'); + expect((where[1] as any).checkOutDate._value).toBe('2026-07-22'); + }); + + it('can query all rooms for automatic settlement', () => { + const where = occupancyWhereOnDate('2026-07-22'); + expect(where.every((condition) => condition.roomId === undefined)).toBe(true); + }); +}); diff --git a/apps/server/src/rooms/room-occupancy-date.ts b/apps/server/src/rooms/room-occupancy-date.ts new file mode 100644 index 0000000..2949720 --- /dev/null +++ b/apps/server/src/rooms/room-occupancy-date.ts @@ -0,0 +1,13 @@ +import { FindOptionsWhere, IsNull, LessThanOrEqual, MoreThan } from 'typeorm'; +import { Occupancy } from '../entities/occupancy.entity'; + +export function occupancyWhereOnDate( + date: string, + roomId?: number, +): FindOptionsWhere[] { + const scope = roomId === undefined ? {} : { roomId }; + return [ + { ...scope, checkInDate: LessThanOrEqual(date), checkOutDate: IsNull() }, + { ...scope, checkInDate: LessThanOrEqual(date), checkOutDate: MoreThan(date) }, + ]; +} diff --git a/apps/server/src/rooms/rooms.service.ts b/apps/server/src/rooms/rooms.service.ts index 299bb69..cfcb104 100644 --- a/apps/server/src/rooms/rooms.service.ts +++ b/apps/server/src/rooms/rooms.service.ts @@ -8,7 +8,6 @@ import { Not, In, LessThanOrEqual, - MoreThanOrEqual, } from 'typeorm'; import { Room } from '../entities/room.entity'; @@ -20,6 +19,7 @@ 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'; @Injectable() export class RoomsService { @@ -246,10 +246,7 @@ export class RoomsService { }); const occupancies = await this.occRepo.find({ - where: [ - { checkInDate: LessThanOrEqual(targetDate), checkOutDate: IsNull() }, - { checkInDate: LessThanOrEqual(targetDate), checkOutDate: MoreThanOrEqual(targetDate) }, - ], + where: occupancyWhereOnDate(targetDate), relations: ['student', 'student.organization', 'responsibleOrganization', 'bed'], order: { checkInDate: 'ASC' }, });