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 { BadRequestException, Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
||||||
import { Cron } from '@nestjs/schedule';
|
import { Cron } from '@nestjs/schedule';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
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 { Bed } from '../entities/bed.entity';
|
||||||
import { Occupancy } from '../entities/occupancy.entity';
|
import { Occupancy } from '../entities/occupancy.entity';
|
||||||
import { Room } from '../entities/room.entity';
|
import { Room } from '../entities/room.entity';
|
||||||
import { RoomInspection } from '../entities/room-inspection.entity';
|
import { RoomInspection } from '../entities/room-inspection.entity';
|
||||||
import { RoomInspectionDetail } from '../entities/room-inspection-detail.entity';
|
import { RoomInspectionDetail } from '../entities/room-inspection-detail.entity';
|
||||||
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||||
|
import { occupancyWhereOnDate } from './room-occupancy-date';
|
||||||
|
|
||||||
interface InspectorIdentity {
|
interface InspectorIdentity {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -211,10 +212,7 @@ export class RoomInspectionsService implements OnApplicationBootstrap {
|
|||||||
|
|
||||||
private findOccupanciesForDate(manager: EntityManager, roomId: number, date: string) {
|
private findOccupanciesForDate(manager: EntityManager, roomId: number, date: string) {
|
||||||
return manager.getRepository(Occupancy).find({
|
return manager.getRepository(Occupancy).find({
|
||||||
where: [
|
where: occupancyWhereOnDate(date, roomId),
|
||||||
{ roomId, checkInDate: LessThanOrEqual(date), checkOutDate: IsNull() },
|
|
||||||
{ roomId, checkInDate: LessThanOrEqual(date), checkOutDate: MoreThanOrEqual(date) },
|
|
||||||
],
|
|
||||||
relations: ['student', 'bed'],
|
relations: ['student', 'bed'],
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
@@ -222,10 +220,7 @@ export class RoomInspectionsService implements OnApplicationBootstrap {
|
|||||||
|
|
||||||
private findAllOccupanciesForDate(manager: EntityManager, date: string) {
|
private findAllOccupanciesForDate(manager: EntityManager, date: string) {
|
||||||
return manager.getRepository(Occupancy).find({
|
return manager.getRepository(Occupancy).find({
|
||||||
where: [
|
where: occupancyWhereOnDate(date),
|
||||||
{ checkInDate: LessThanOrEqual(date), checkOutDate: IsNull() },
|
|
||||||
{ checkInDate: LessThanOrEqual(date), checkOutDate: MoreThanOrEqual(date) },
|
|
||||||
],
|
|
||||||
relations: ['student', 'bed'],
|
relations: ['student', 'bed'],
|
||||||
order: { roomId: 'ASC', id: 'ASC' },
|
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,
|
Not,
|
||||||
In,
|
In,
|
||||||
LessThanOrEqual,
|
LessThanOrEqual,
|
||||||
MoreThanOrEqual,
|
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { Room } from '../entities/room.entity';
|
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 { CreateBedDto, UpdateBedDto, BatchCreateBedDto } from './dto/bed.dto';
|
||||||
import { CreateLockerDto, UpdateLockerDto, BatchCreateLockerDto } from './dto/locker.dto';
|
import { CreateLockerDto, UpdateLockerDto, BatchCreateLockerDto } from './dto/locker.dto';
|
||||||
import { RoomInspectionsService } from './room-inspections.service';
|
import { RoomInspectionsService } from './room-inspections.service';
|
||||||
|
import { occupancyWhereOnDate } from './room-occupancy-date';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RoomsService {
|
export class RoomsService {
|
||||||
@@ -246,10 +246,7 @@ export class RoomsService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const occupancies = await this.occRepo.find({
|
const occupancies = await this.occRepo.find({
|
||||||
where: [
|
where: occupancyWhereOnDate(targetDate),
|
||||||
{ checkInDate: LessThanOrEqual(targetDate), checkOutDate: IsNull() },
|
|
||||||
{ checkInDate: LessThanOrEqual(targetDate), checkOutDate: MoreThanOrEqual(targetDate) },
|
|
||||||
],
|
|
||||||
relations: ['student', 'student.organization', 'responsibleOrganization', 'bed'],
|
relations: ['student', 'student.organization', 'responsibleOrganization', 'bed'],
|
||||||
order: { checkInDate: 'ASC' },
|
order: { checkInDate: 'ASC' },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,3 +117,54 @@ describe('WalletsService financial boundaries', () => {
|
|||||||
expect(dataSource.transaction).not.toHaveBeenCalled();
|
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) {
|
if (!lock || !manager.createQueryBuilder) {
|
||||||
return manager.findOne(StudentWallet, { where: { studentId } });
|
return manager.findOne(StudentWallet, { where: { studentId } });
|
||||||
}
|
}
|
||||||
return manager.createQueryBuilder(StudentWallet, 'wallet')
|
let query = manager
|
||||||
.where('wallet.studentId = :studentId', { studentId })
|
.createQueryBuilder(StudentWallet, 'wallet')
|
||||||
.setLock('pessimistic_write')
|
.where('wallet.studentId = :studentId', { studentId });
|
||||||
.getOne();
|
if (['mysql', 'mariadb', 'postgres', 'cockroachdb'].includes(this.dataSource.options.type)) {
|
||||||
|
query = query.setLock('pessimistic_write');
|
||||||
|
}
|
||||||
|
return query.getOne();
|
||||||
};
|
};
|
||||||
let wallet = await find();
|
let wallet = await find();
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
|
|||||||
Reference in New Issue
Block a user