Merge pull request '修正换宿当天查寝归属、修复 SQLite 钱包调账锁异常' (#36) from xiongyuxing/gongxue-base:main into main
This commit is contained in:
@@ -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' },
|
||||
});
|
||||
|
||||
22
apps/server/src/rooms/room-occupancy-date.spec.ts
Normal file
22
apps/server/src/rooms/room-occupancy-date.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
13
apps/server/src/rooms/room-occupancy-date.ts
Normal file
13
apps/server/src/rooms/room-occupancy-date.ts
Normal file
@@ -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<Occupancy>[] {
|
||||
const scope = roomId === undefined ? {} : { roomId };
|
||||
return [
|
||||
{ ...scope, checkInDate: LessThanOrEqual(date), checkOutDate: IsNull() },
|
||||
{ ...scope, checkInDate: LessThanOrEqual(date), checkOutDate: MoreThan(date) },
|
||||
];
|
||||
}
|
||||
@@ -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' },
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user